ツナ缶雑記

ぐうたらSEのブログです。主にマイクロソフト系技術を中心に扱います。

Blazorの概要

ASP.NET Core 3.0もリリースされて、サーバーサイドレンダリング限定ですが、Blazorも使えるようになりました。 ようやく触り始めたので自分の勉強メモもかねて、残していきたいと思います。

Blazorって何?

公式サイトを見ると、

.NETを使って対話型のクライアント側Web UIを構築するためのフレームワーク と説明されています。

軽く触った感じだと、論理的なアプリケーション構造はASP.NET Web Formsと似ています。 画面をコンポーネントという単位にまとめて、それらを画面に張り付けていくスタイルです。 Web FormsでいうところのUserControlに近い感覚でしょうか。 実際にWeb Formsの後継として位置づけられることもあるようですね。

動作の仕組みと他の技術との比較

ただ、実際の動作はWeb Formsとは似ても似つかない動きをします。 現時点で使えるBlazorサーバーは、簡単に言うと「サーバーサイドでDOMの構築を行うSingle Page Application」です。 通常AngularやVue.jsなどのSingle Page Applicationは、以下のようにブラウザー上のJavaScriptでDOMの構築を行います。

f:id:masatsuna:20191022153728p:plain
Single Page Applicationの動作

BlazorサーバーはこのDOM構築作業をすべてサーバー上で行い、ブラウザーにはDOMを送り付けるような動作になります。 通信はSignalRによって行われており、あたかもJavaScriptでDOMを構築しているかのような動きをするのです。

f:id:masatsuna:20191022153800p:plain
Blazorサーバーの動作

SignalRは通信機器が対応していればWebSocketによる通信となるため、通信量は非常に軽くなります。 こういう技術的な下支えがあって、Blazorサーバーは動作しているということになります。

もう少しすると、Blazor WebAssemblyと呼ばれる仕組みも使えるようになります。 こちらはJavaScriptのSingle Page Applicationと同様、ブラウザー上でDOMの構築を行います。 ただし、その処理はBlazor WebAssemblyで行います。 Blazor WebAssemblyは.NETで実装することになります。 これにより、JavaScriptの実装なしでSingle Page Applicationが実装できてしまうのです。 わざわざJavaScriptフレームワークを覚える必要がなくなります。

f:id:masatsuna:20191022154706p:plain
Blazor WebAssemblyの動作

結構とんでもないフレームワークだなーという印象です。

まとめ

クライアントサイドの実装はここのところJavaScript系のフレームワークが台頭していて、生産性重視のエンタープライズシステムでは採用しにくい状況が続いていました。 .NETもJavaScriptのSingle Page Applicationフレームワークも両方使える技術者さんってそんなに多くなくて、開発者を集めるのに苦労していたんですよね。 Blazorはこういった状況を打破してくれる可能性を秘めていると思います。

とはいえ、実装技術だけを見ると、どうしてもシステムリソースの消費量が気になってきます。 もう少しこの辺りは調査が必要な気がしますね。

Azure PipelinesからAzure ArtifactsにNuGetパッケージを公開する

やりたいこと

前回作成した NuGet パッケージを、 Azure Artifacts にアップロードして、プライベートな NuGet パッケージリポジトリを構築します。

NuGet パッケージのもとになるソースコード一式は、 Azure DevOps の Git リポジトリに配置します。 その後、ビルドパイプライン、リリースパイプラインを通して、 Azure Artifacts にアップロードできるようにしたいと思います。

前提条件

NuGet パッケージを作成するもとになるソースコード一式を Azure DevOps の Git リポジトリに入れておいてください。

Azure Artifacts のフィードを作成する

プライベートな NuGet パッケージリポジトリを作成するにあたり、まずは Azure Artifacts にその場となるフィードを作成します。

Azure DevOps にログインして、 [Artifacts] のメニューを選択してください。 以下のような画面になったら、画面中央の [+ New feed] ボタンを押下します。

f:id:masatsuna:20190923235218p:plain
フィードの追加

フィードの名前や公開範囲の設定を行います。 今回はこの組織に属しているユーザー間でのみ共有するプライベートな NuGet リポジトリにしたいので、以下のような設定としました。

f:id:masatsuna:20190924000950p:plain
新しいフィードの設定

フィードの作成が完了すると、以下のようになります。

f:id:masatsuna:20190924001205p:plain
フィード作成後

ビルドパイプラインを構築する

続いて Git リポジトリに登録したソースコードをビルドするためのビルドパイプラインを作成します。 ビルドパイプラインには、ソースコードをビルドして、リリースするための資材を作る役割を持たせます。 今回のリリース資材は、NuGetパッケージそのものである .nupkg ファイルです。 ビルドパイプライン内で、 .nupkg ファイルを作成していきます。

Azure DevOps の [Pipelines] メニューを開き、 [Pipelines] を選択します。

f:id:masatsuna:20191004235723p:plain
Pipelines を選択

画面右上の [New pipeline] ボタンを押下して、ソースコードの場所を選択します。 今回は Azure DevOps の Git リポジトリソースコードがあるので、 [Azure Repos Git] を選択します。

f:id:masatsuna:20191004235912p:plain
ソースコードの場所を選択

次にリポジトリを選択します。 ソースコードを配置したリポジトリを選択してください。

次にビルドパイプラインのテンプレートを選択します。 今回は .NET Framework の NuGet パッケージを作成するので、最も近い [.NET Desktop] を選択します*1

f:id:masatsuna:20191005000235p:plain
パイプラインのテンプレートを選択

作成したyamlファイルの全体像

今回は簡単に作るので、生成されたテンプレートをほぼそのまま使用し、 NuGet パッケージのビルドと、ビルド Artifact のアップロード処理を追加しています。 こちらがyamlファイルの全体像です。

trigger: none

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

steps:
- task: NuGetToolInstaller@1

- task: NuGetCommand@2
  inputs:
    restoreSolution: '$(solution)'

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: VSTest@2
  inputs:
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'

- task: NuGetCommand@2
  displayName: 'NuGet パッケージの作成'
  inputs:
    command: 'pack'
    packagesToPack: '**/AppSettings.Core.csproj'
    versioningScheme: 'off'

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'AppSettingsCore'
    publishLocation: 'Container'

以下個別に実装内容を解説します。

trigger

ビルドトリガーを設定する箇所です。 ここは唯一テンプレートから変更しています。 今回は手動でビルドパイプラインをキックするので、 trigger の設定値を none にしています。 CI を行いたい場合は、ここにブランチ名を設定していきます。

pool

ビルドを実行する場所を設定します。 今回はマイクロソフトの提供するビルドエージェントを使うので、 [windows-latest] を設定しています。 選択可能なエージェント名は、以下のページから確認できます。

docs.microsoft.com

自分専用のビルドマシンを使うこともできますが、ここでは割愛します。

variables

ビルドパイプライン内で使用する変数を定義します。 ここに定義した変数は「$(<変数名>)」とすることで、後から参照することができます。

steps

ビルドの処理ステップを定義します。

task: NuGetToolInstaller@1

NuGet.exe をダウンロードします。 今回の例では何も設定を行っていません。 何も設定を行わないと、最新の NuGet.exe がダウンロードされます。

task: NuGetCommand@2 1つ目

アプリケーションの使用する NuGet パッケージを復元します。 Visual Studio で実行する [NuGet パッケージの復元] と同じことを行ってくれます。

task: VSBuild@1

アプリケーションをビルドします。 この例では Release モードでソリューション一式をビルドしています。

task: VSTest@2

アプリケーションに含まれるテストを実行します。 今回は特に設定していませんが、テストを検索するフォルダなどを細かく設定することもできます。

task: NuGetCommand@2 2つ目

NuGet パッケージを作成します。 前回まで行っていた nuget pack コマンドに相当する処理です。 NuGet パッケージとしてビルドするプロジェクトのパスを指定して、 .nupkg ファイルを作成します。 作成した .nupkg ファイルは、既定でビルド Artifact をまとめるディレクトリ(変数名でいうと、Build.ArtifactStagingDirectory)にコピーされます。

task: PublishBuildArtifacts@1

[Build.ArtifactStagingDirectory] のディレクトリに配置されているビルド成果物一式をビルド Artifact としてアップロードします。 [ArtifactName] にわかりやすい名前を付けるようにしてください。

ビルドパイプラインの実行

ここまで作業を進めたら、ビルドパイプラインを保存して、実行しておきましょう。

f:id:masatsuna:20191005004953p:plain
ビルドパイプラインの実行

リリースパイプラインを構築する

続いてビルド Artifact としてアップロードした *.nupkg ファイルを Azure Artifacts に公開するためのリリースパイプラインを作成します。 リリースパイプラインには、ビルド Artifact に登録した資材を配置する役割を持たせます。

Azure DevOps の [Pipelines] メニューを開き、 [Releases] を選択します。 続いて [New release pipeline] ボタンを押下します。

f:id:masatsuna:20191004230151p:plain
テンプレートの選択

テンプレートの選択画面になりますが、今回は上部の [Empty job] を選択します。

f:id:masatsuna:20191004230005p:plain
空のリリースパイプライン

リリースパイプラインを作成すると、上記のようにどの資材をどこに配置するかを選択する画面になります。 まずはリリースする資材を選択するため、左側の [Artifacts] の囲みの中にある [Add an artifact] を押下します。

f:id:masatsuna:20191004225915p:plain
ビルド Artifact の選択

今回はビルド Artifact を資材として使用するので、 [Source type] は [Build] を選択します。 [Source (build pipeline)] のドロップダウンで、作成したビルドパイプラインの名前を選択します。 [Default version] は適当に設定してください。 [Source alias] に設定する値は、この資材の識別名みたいなものなのですが、リリースパイプライン内でたくさんのビルド資材を扱わない限り、あまり設定値を気にする必要ありません。 とりあえずデフォルトのままでも大丈夫です。

続いて配置先の設定を行っていきます。 上部の [Tasks] タブを選択します。

f:id:masatsuna:20191004230839p:plain
タスクの追加

今回は NuGet.exe の機能を使用して、 Azure Artifacts に NuGet パッケージをアップロードします。 ですので、 [NuGet tool installer] と [NuGet] のタスクを図のように追加しておきます。

f:id:masatsuna:20191004231433p:plain
NuGet tool installer の設定

[NuGet tool installer] のタスクでは、インストールする NuGet.exe のバージョンを設定します。 今回は特にこだわりがないので、バージョンを未指定のまま [Always check for new versions] にチェックを入れて、常に最新の Nuget.exe をダウンロードしてくるように設定しておきます。

f:id:masatsuna:20191004231311p:plain
nuget push の設定

[NuGet] のタスクではビルドパイプラインで作成した nupkg ファイルを、作成済みのフィードにアップロードするよう設定します。 [Command] のドロップダウンは [push] を選択します。 [Push to NuGet package(s) to publish] は、アップロードする nupkg ファイルを選択します。 右側にある […] ボタンを押下すると、アップロード済みのビルド Artifact から、アップロードする nupkg ファイルを GUI で選択できるため便利です。 ただし、 nupkg ファイルにはバージョン番号が含まれるため、この部分はワイルドカード文字 [*] を使用しておくことをおすすめします。 最後にアップロード先のフィードを選択します。 今回は先に作成したフィードに対してアップロードしたいので、 [Target feed location] を [This organization/collection] に設定し、 [Target feed] のドロップダウンから作成したフィード名を選択します。

設定が完了したら、画面上部の [Save] ボタンを押下して保存してから、 [Create release] ボタンを押下します。

f:id:masatsuna:20191004232502p:plain
リリースの実行

リリースの設定画面になるので、リリースするビルド Artifact のバージョンを選択します。 特に何も設定を行っていない場合、最後のビルド Artifact が既定で選択されているはずです。 [Create] ボタンを押下すると、 NuGet パッケージが Azure Artifacts にアップロードされます。

f:id:masatsuna:20191005132724p:plain
Azure Artifacts にアップロード

ビルド完了後に自動的にリリースするよう設定する

ここまでの作業を行うと、ビルドパイプラインとリリースパイプラインをそれぞれバラバラに起動して、 NuGet パッケージのビルド~公開までができるようになります。 さらに自動化を進め、ビルドパイプラインが正常に終了したら、自動的にリリースパイプラインを実行するよう構成することもできます。

先ほど作成したリリースパイプラインの編集画面に入り、ビルド Artifact の右上にある雷ボタン、 [Continuous deployment trigger] ボタンを押下します。

f:id:masatsuna:20191005133301p:plain
継続的配信トリガーの追加

設定画面が表示されるので、設定を行います。

f:id:masatsuna:20191005133519p:plain
継続的配信トリガーの設定

まず [Continuous deployment trigger] のスイッチを [Enabled] に設定します。 そして、どのブランチに対するビルドであったときに、継続的配信を実行するかを決めます。 例えば Github-Flow で開発を行っているのであれば、 master ブランチ以外をリリースしてはいけないので、図のように master ブランチに対するビルド時のみリリースを行う、といった設定ができます。 この辺りは採用する Git 運用によっていろいろ設定が変わってくるところなので、いい感じの設定を見つけてもらえればと思います。 設定が完了したら変更を保存しましょう。

これで、ビルドパイプラインに master ブランチをかけたとき、自動的に NuGet パッケージが Azure Artifacts にアップロードされるようになりました。

*1:多分近いうちに、 .NET Core 向けの選択肢が増えるので、名前が変わるんじゃないかと思います。

構成ファイルの書き換えを行うNuGetパッケージの作り方(XDT変換編)

やりたいこと

NuGetパッケージをプロジェクトに追加したとき、勝手に構成ファイルが書き換わるパッケージというのが存在するかと思います。 Entity Framework 6.2.0とかもそうで、構成ファイルに設定が入ってきます。 このように、構成ファイルを書き換えるNuGetパッケージの作り方をまとめたいと思います。

実はこのような構成ファイルの書き換えを行う方法は2つあるのですが、今回はかなり細かくいろいろ制御できるXDT変換という手法について解説します。 もっとお手軽なやり方としてXML変換というものもあります。 こちらについては以前の記事でまとめていますので、そちらをご覧ください。

tsuna-can.hateblo.jp

なお本稿ではNuGetパッケージの作り方を詳細には解説しません。 作り方が知りたい場合は以下の記事をまずご覧ください。

tsuna-can.hateblo.jp

環境

前提条件

構成ファイルの自動書き換えが行われるのはpackages.configを使っているプロジェクトから今回作成するNuGetパッケージを参照する場合のみです。 PackageReferenceを使っている場合、構成ファイルの書き換えは行われませんので注意してください。

やり方

NuGetパッケージのプロジェクトに構成ファイルの変換設定を追加する

作成するNuGetパッケージ内に「app.config.install.xdt」、「app.config.uninstall.xdt」、「web.config.install.xdt」、「web.config.uninstall.xdt」という名前のファイルを配置しておき、そこにNuGetパッケージの参照を追加・削除した際、構成ファイルを変換する設定を行います。 .install.xsdファイルが参照追加時に適用するXDTファイルで、.uninstall.xsdファイルが、参照削除時に適用するXDTファイルです。 XML変換の時は参照追加時も削除時も、同一のファイルを使用して変換を行っていましたが、XDT変換ではシーンごとに使用するXSDファイルが変わります。 まずはこれらのファイル(以降、XDTファイル)をプロジェクトにXMLファイルとして追加します。

f:id:masatsuna:20190906223526p:plain
XDTファイルの追加

この例ではConfigTransformフォルダを作って、その中にXDTファイルを配置しています。 この段階では、名前さえ合っていれば、どこにファイルを配置してもかまいません。

参照追加時の変換設定

続いて作成したXDTファイルの中身を編集します。 今回は、このNuGetパッケージを参照した際、構成ファイルのAppSettingsセクションに要素を1つ追加するようにしてみます。 またこの時、既存の構成ファイルに同名のキーが存在する場合は、変換を行わいようにしてみます。 「app.config.install.xdt」と「web.config.install.xdt」のXDTファイルを以下のように設定します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appSettings xdt:Transform="InsertIfMissing">
    <add key="test-key" value="test-val" xdt:Transform="InsertIfMissing" xdt:Locator="Match(key)"/>
  </appSettings>
</configuration>

XML変換の時と同様、全体的な構造は、構成ファイルの構造と同じです。 構成ファイルのルート要素は configuration 要素なので、XDTファイルも同様のルート要素を定義します。 ただし、今回はXDT変換を行いたいので、 configuration 要素にxdt名前空間の定義を行っておきます。 よくわからない人はコピペでOKです。

続いて追加する要素の設定を行っています。 今回は appSettings 要素の中に add 要素を定義したいので、それをまずそのまま書きましょう。 そして、先ほど定義した xdt 名前空間の属性を用いて、その要素をどのように適用するかを定義します。

今回は appSettings 要素に xdt:Transform="InsertIfMissing" を定義しています。 これは、変換元の構成ファイルに appSettings 要素がなければ追加する、という意味です。 この xdt:Transform 属性に設定できる値はいくつかあります。 以下のリンク先に解説があるのですが、実は InsertIfMissing については解説がありません。 XDT変換をまじめにやろうとすると、これは多用することになるんですが、なんでか書かれていないんですよね。

Visual Studio を使用する Web アプリケーション プロジェクト配置の Web.config 変換構文 | Microsoft Docs

add 要素も同様、 xdt:Transform="InsertIfMissing" が付与されていますので、存在しないときだけ追加します。 ただし、 add 要素には xdt:Locator="Match(key)" が指定されています。 これは要素を絞り込むための属性で、この例では add 要素の key 属性値が同じものを探しています。 すなわち、 key 属性値が test-key である add 要素がないときに追加する、という意味になります。 xdt:Locator 属性に指定できる値については、先述のリンク先に解説がありますので参照してください。

このファイルには、このNuGetパッケージを動作させるために必要な設定を書いておくのがよいです。 例えばデータアクセスを行う必要があるなら、その接続文字列の設定例とかがそれにあたります。

参照削除時の変換設定

続いて参照を削除したとき(NuGetパッケージをアンインストールしたとき)の変換設定を行います。 今回は、参照追加時に追加した要素と同じ kye 属性値を持つ要素を削除するようにしてみます。 「app.config.uninstall.xdt」と「web.config.uninstall.xdt」のXDTファイルを以下のように設定します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <appSettings>
    <add key="test-key" xdt:Transform="Remove" xdt:Locator="Match(key)"/>
  </appSettings>
</configuration>

今回削除対象となる add 要素に、 xdt:Transform="Remove" を指定して、削除することを設定しています。 また xdt:Locator="Match(key)" を指定して、 key 属性値が同じ要素を削除するようにしています。

追加したXDTファイルのビルドアクションを確認する

XMLファイルをプロジェクトに追加すると、通常ビルドアクションは「なし」になりますが、念のためちゃんと確認しておきましょう。 追加した変換設定ファイルを右クリックして[プロパティ]を選択します。

f:id:masatsuna:20190906230658p:plain
ビルドアクションの確認

これが「コンテンツ」になっていると、NuGetパッケージをビルドしたときまったく別の場所に配置されてしまう問題が発生します。 必ず「なし」になっていることを確認します。

*.nuspecファイルに作成したXDTファイルの設定を追加する

次に、追加したXDTファイルをNuGetパッケージ内に含めるよう、以下のように*.nuspecファイルを編集します。

<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <title>$title$</title>
    <authors>$author$</authors>
    <owners>$author$</owners>
    <licenseUrl>https://tsuna-can.hateblo.jp/</licenseUrl>
    <projectUrl>https://tsuna-can.hateblo.jp/</projectUrl>
    <iconUrl>https://tsuna-can.hateblo.jp/</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$description$</description>
    <releaseNotes>初版リリース。</releaseNotes>
    <copyright>$copyright$</copyright>
    <tags>サンプル</tags>
  </metadata>
  <files>
    <file src="ConfigTransform\*.xdt" target="content" />
  </files>
</package>

以前の記事で作成した*.nuspecファイルに、files要素を追加しています。 files要素は、NuGetパッケージ内に含めるファイルを追加するための要素です。 ここでは、先ほど作成したXDTファイルのパスをワイルドカード文字を使って設定しています。 target属性には、src属性に指定したファイルをNuGetパッケージ内のどこに配置するかを指定します。

XDTファイルの配置場所は、NuGetの仕様でcontentディレクトリ内直下、と定められています。 そのため、target属性には「content」を指定し、そのディレクトリ内にXDTファイルが配置されるようにしているわけです。

ここまででNuGetパッケージ追加時の動作設定は完了です。 一旦プロジェクトをReleaseに設定してビルドしておきます。

NuGetパッケージを作成する

続いて、NuGetパッケージの作成を行います。 前回とまったく同じコマンドで、NuGetパッケージを作成できます。

C:\XXXXX\AppSettings.Core>nuget pack -Properties Configuration=Release
'AppSettings.Core.csproj' からパッケージをビルドしています。
MSBuild auto-detection: using msbuild version '16.2.37902.0' from 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\bin'.
'C:\XXXXX\AppSettings.Core\bin\Release' のファイルをパックしています。
メタデータに 'AppSettings.Core.nuspec' を使用しています。
packages.config が見つかりました。依存関係として登録されているパッケージを使用します
Successfully created package 'C:\XXXXX\AppSettings.Core\AppSettings.Core.1.0.7188.41693.nupkg'.
警告: NU5125: The 'licenseUrl' element will be deprecated. Consider using the 'license' element instead.

特に警告に対する対処も行っていないので、同じ警告が出ています。 が、ここは無視して先に進みます。

NuGetパッケージの中身を確認する

では早速ビルドして出来上がった*.nupkgファイルの中身を確認してみます。 *.nupkgファイルはzip圧縮されているので、拡張子をzipに変更して展開すれば、中身を確認できます。 以下が今回作成した*.nupkgファイルを展開した中身です。

f:id:masatsuna:20190906231302p:plain
nupkgファイルの中身

contentディレクトリ内を見てみると、以下のように今回追加したXDTファイルが含まれているのがわかります。

f:id:masatsuna:20190906231403p:plain
XDTファイル

NuGetパッケージとして追加して動作を確認する

作成したNuGetパッケージを実際に使って、動作確認を行ってみます。 適当にコンソールアプリケーションプロジェクトを作成して、今回作成したNuGetパッケージを参照に追加してみます。 ローカル環境で動作確認するための方法は、前回記事を参考にしてください。

シンプルな追加の場合

Visual Studio.NET Frameworkのコンソールアプリケーションを作成すると、勝手にApp.configファイルが含まれていると思います。 私の環境では以下のようなApp.configファイルが自動的に生成されました。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
</configuration>

今回作成したNuGetパッケージを参照に追加すると、この構成ファイルが以下のように勝手に書き換わります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
  <appSettings>
    <add key="test-key" value="test-val" />
  </appSettings>
</configuration>

XDTファイルに記載した内容が構成ファイルに対して設定されており、ちゃんと変換設定が行われたことが確認できます。 Web.configについては特に記載しませんが、同様の動作が確認できると思います。

既に同じキーを持つ要素がある場合

今回はXDT変換を使用したため、前回できなかったこまかな制御ができるようになっています。 例えばもともとの構成ファイルが以下のような状態だったとき、XML変換とXDT変換でどのような差が出るでしょうか。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="test-key" value="pre set value"/>
  </appSettings>
</configuration>

まずXML変換の場合は、以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="test-key" value="pre set value" />
    <add key="test-key" value="test-val" />
  </appSettings>
</configuration>

add 要素には、 key 属性が同じ値の要素があるのですが、それを完全に無視して自分の追加したい要素を追加しています。 前回も述べた通り、XML変換は完全一致しているかどうかで追加するかどうかを判定します。 そのため、 value 属性の値が異なる要素を別の要素だと判定してしまい、要素ごと追加されてしまったわけです。

それに対して、今回作成したXDT変換の場合はこうなります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <appSettings>
    <add key="test-key" value="pre set value"/>
  </appSettings>
</configuration>

XDTファイルで key 属性値が一致するものは、同一の要素だと判定するようにXDTファイルを実装したので、もともとの構成ファイルに設定されていた値をそのまま使用し、変換を行いません。 こういったちょっとした気遣いが実装できるのはXDT変換だけです。

ただし、構成ファイルがプロジェクトに存在しない場合、XML変換と異なり、構成ファイルの追加が行われません。 この点はXML変換と比較して機能的に落ちるので注意してください。

NuGetパッケージをアンインストールしたときの挙動

NuGetパッケージをアンインストールすると、参照削除時に動作するように設定したXDTファイルが適用されます。 今回は key 属性値が「test-key」である要素を削除するように実装していますので、インストール時に追加された add 要素が削除されることになります。

XML変換の場合、要素の削除も完全一致した要素しか行われませんでした。 XDT変換ならある程度柔軟性を持たせることができるため、今回のように条件に合致する要素だけ消す、といったことも可能です。 またXDTファイルは、インストール時に使用するものとアンインストール時に使用するものが分かれていますので、インストール時に追加だけして削除しない、といったこともやろうと思えばできます。

まとめ

全2回にわたって、NuGetパッケージの参照追加時に行われる構成ファイルの自動書き換え方法を見てきました。 簡単に実施するだけならXML変換を、より柔軟な変換が求められる場合はXDT変換を使用するとよいと思います。

またXDT変換を使用する際、最もよく使うと思われる xdt:Transform 属性の値、「InsertIfMissing」が公式ドキュメントに記載されていない*1ことを書きました。 これを使うと、かなり柔軟な変換が実現できますのでおすすめです。

*1:私がこの設定値を知ったのは、どうやってみんな設定しているのか調べるために、Entity Frameworkの*.install.xsdを眺めたのがきっかけです。まさか公式ドキュメントに書かれていないものがあるとは発想すらなかった。。。

Visual StudioなしでAzure DevOpsのTFVCリポジトリにコマンドラインで接続する方法

何をいまさら。。。

正直普通にやるならAzure DevOpsのBuild Agentに登録して、Azure Pipelinesのタスクでやるのが王道だと思います。 その方が圧倒的に簡単だし楽だし、悪いことはほぼないでしょう。 ですが、今回事情によってどうしてもそういう作戦が取れず、何とか無理やり実現したことをまとめておきます。

歴史を振り返る

TFVC(Team Foundation Version Control)のクライアントツールってほとんど存在しません。 そもそもTFVC自体がGitに押されまくってあまり使われていないのと、MSの製品なので、Windows + Visual Studioが前提だった背景があるんだろうなーと想像しています。 基本的にTFVCのリポジトリにアクセスするためには、Visual Studioに付属するチームエクスプローラーを用いることになっています。 コマンドラインから操作する場合も、Visual Studioに付属する「tf.exe」を用いてアクセスするのが王道です。

そんな中、2012年にEclipseのマーケットプレースに、チームエクスプローラーのプラグインであるTeam Explorer Everywhereが追加されました。 今振り返ってみれば、Team Foundation Serverをもっと広げていくための布石として作ったんだなーと思ったりもするわけです。 このプラグインがリリースされてから、Team Foundation ServerVisual Studio OnlineとしてSaaSサービス化され、Visual Studio Team Services、Azure DevOpsとリブランディング(という名の改名=迷走)を繰り返し今に至っています。 Java系の開発者もTeam Foundation ServerとかAzure DevOpsのエコシステムに取り込むための先行投資として、こういったプラグインが作られたのでしょう。

グダグダ書きましたが、結論このEclipse用のプラグインを使えば、Visual StudioなしでAzure DevOps上のTFVCにアクセスることができます。 コマンドラインも本家Visual Studio付属のtf.exeと似たような体系になっています。

Team Explorer Everywhere

さて、Team Explorer Everywhereですが、残念なことに2018年で開発が終了しています。 ご存知の通り、既に構成管理システムはGitがデファクトになっており、MSがEclipse向けのプラグインをわざわざ提供する必要性や価値がなくなったものと思います。 Gitのクライアント向けEclipseプラグインならほかにもいろいろありますからね。

ただ、ありがたいことにTeam Explorer Everywhereの最終版は、現在もGithubリポジトリからダウンロードすることができます。

github.com

今回はこのツールを使って、TFVCリポジトリにアクセスしようと思います。

導入手順

Javaのインストール

Team Explorer Everywhereのコマンドラインツールは、Javaで作成されています。 使用するマシン上にJavaのランタイムを入れておきましょう。 GitHubの解説では、Java 6を入れろと書かれていますので、お好きに*1インストールしてパスを通しておきましょう。

コマンドラインツールを配置してパスを通す

準備が整ったので、Team Explorer Everywhereを導入していきます。 Team Explorer Everywhereは、Eclipse用のプラグイン部分と、そのプラグインから使用するツールが分割配置されています。 先ほどのGithubリポジトリのReleaseタブを見てもらうと、以下のように3つの構成物があることがわかります。

f:id:masatsuna:20190825141245p:plain
Team Explorer Everywhere最終版

今回はコマンドラインからTFVCのリポジトリに接続したいので、「TEE-CLC-14.134.0.zip」をダウンロードします。 ダウンロードが完了したら、ブロックの解除を行ってからzipファイルを展開し、お好きな場所に配置します。

f:id:masatsuna:20190825155215p:plain
Team Explorer Everywhereの展開

後程このディレクトリにある「tf.cmd」をコマンドプロンプトから叩くことになるので、パスを通しておきましょう。 パスが通った状態で、コマンドプロンプトから

tf /help

を実行すると、tf.cmdの使い方を知ることができます。

f:id:masatsuna:20190825155437p:plain
tf /helpの実行

Team Explorer Everywhereのライセンスに同意する

このコマンドラインツールは、実行前にライセンスに同意しておかないと使えません。 まずコマンドプロンプトから以下のコマンドを実行します。

tf eula

すると以下のようにライセンス条項が流れてきます。

f:id:masatsuna:20190825160738p:plain
エンドユーザーライセンス

ちゃんと読んでEnterキーを押下しながら最後まで進めましょう。 最後に同意するかどうか聞かれます。 Enterキーを連打しすぎるとこの選択肢もすっ飛ばしてしまうので注意です。

f:id:masatsuna:20190825161236p:plain
ライセンスへの同意

Azure DevOpsのTFVCにアクセスする

Azure DevOpsでPATを発行する

続いてTFVCリポジトリのあるAzure DevOps組織にログインして、PATを発行します。 Full accessの権限が必要*2ですので、割り当てておきましょう。

コマンドラインからローカルのワークスペースを作成する

ここからはTeam Explorer Everywhereのコマンドラインツールを使用していきます。 コマンドプロンプトを管理者権限で起動してください。 tf workspaceコマンドを使用してローカルワークスペースを作成します。

tf workspace /new /location:local TEE-Test /collection:<Azure DevOpsの組織のURL> /login:<Azure DevOpsのログインユーザー名(メールアドレス)>,<先ほど取得したPATの値>
Workspace 'TEE-Test' created.

ここで注意してほしいのは、/collectionスイッチに指定するURLは、Azure DevOpsの組織のURLである点です。 TFVCのリポジトリがあるプロジェクトのURLではありません。 なお/loginスイッチは未指定でも実行可能です。 その場合、ユーザー名とパスワードをインタラクティブに聞いてきます。 この場合も、パスワードはPATの値になるので注意してください。

ワークフォルダーのマップを作成する

次にローカルマシン上のワークフォルダーをtf workfoldコマンドで作成します。

tf workfold /map /workspace:TEE-Test $/TEE-Test C:\Repos\TEE-Test /collection:<Azure DevOpsの組織のURL> /login:<Azure DevOpsのログインユーザー名(メールアドレス)>,<先ほど取得したPATの値>

この例では、指定した組織のAzure DevOps内にある「$/TEE-Test」という名前のTFVCリポジトリとのマップを作成しています。 これで指定したディレクトリ(この例では「C:\Repos\TEE-Test」)に「$tf」という名前の隠しフォルダが作成されます。

ここで指定するディレクトリに後程TFVCのリポジトリが丸ごと落ちてきます。 ですので、なるべく浅いパスを指定しておく方が幸せになれます。

リポジトリを取得する

最後に、作成したディレクトリにTFVCリポジトリのファイル一式をtf getコマンドで取得します。

tf get /recursive /overwrite C:\Repos\TEE-Test /login:<Azure DevOpsのログインユーザー名(メールアドレス)>,<先ほど取得したPATの値>
C:\Repos:
TEE-Test を取得しています

C:\Repos\TEE-Test:
Sample1.txt を取得しています
Sub1 を取得しています

C:\Repos\TEE-Test\Sub1:
Fuga1.txt を取得しています
Fuga2.txt を取得しています
Sub2 を取得しています

これで「C:\Repos\TEE-Test」ディレクトリにTFVCリポジトリの全ファイルが落ちてきます。

まとめ

今回は、Visual Studioをインストールしていない環境でAzure DevOpsのTFVCリポジトリにアクセスする方法を解説しました。 ご紹介したのはTeam Explorer Everywhereの提供するコマンドラインの本当にわずかな部分だけです。 ヘルプを参照すると、もっといろいろ使い方がわかるかと思います。

ただ、今の時代はAzure DevOpsのビルドエージェントに登録してしまえば、こんな面倒なことしなくて済みます。 そういうことがどうしてもできない事情がある場合のみ参考にしてください。

*1:なお私はAdopt Open JDK 8(HotSpot)を入れて動かしてみました。私が使った範囲では問題なく動きました。

*2:調べた範囲だとCodeのFullアクセスでよさそうなんですが、2019/8/25現在、CodeのFullアクセス権のみだとファイルを取得する際401エラーが出てしまいました。

構成ファイルの書き換えを行うNuGetパッケージの作り方(XML変換編)

やりたいこと

NuGetパッケージをプロジェクトに追加したとき、勝手に構成ファイルが書き換わるパッケージというのが存在するかと思います。 Entity Framework 6.2.0とかもそうで、構成ファイルに設定が入ってきます。 このように、構成ファイルを書き換えるNuGetパッケージの作り方をまとめたいと思います。

実はこのような構成ファイルの書き換えを行う方法は2つあるのですが、今回はもっとも簡単なXML変換という手法について解説します。

なお本稿ではNuGetパッケージの作り方を詳細には解説しません。 作り方が知りたい場合は以下の記事をまずご覧ください。

tsuna-can.hateblo.jp

環境

前提条件

構成ファイルの自動書き換えが行われるのはpackages.configを使っているプロジェクトから今回作成するNuGetパッケージを参照する場合のみです。 PackageReferenceを使っている場合、構成ファイルの書き換えは行われませんので注意してください。

やり方

NuGetパッケージのプロジェクトに構成ファイルの変換設定を追加する

作成するNuGetパッケージ内に「app.config.transform」、「web.config.transform」という名前のファイルを配置しておき、そこにNuGetパッケージの参照を追加した際、構成ファイルに追加する設定を行います。 まずは「app.config.transform」ファイル、「web.config.transform」ファイル(以降、変換設定ファイル)をプロジェクトにXMLファイルとして追加します。

f:id:masatsuna:20190810234123p:plain
変換設定ファイルの追加

この例ではConfigTransformフォルダを作って、その中に変換設定ファイルを配置しています。 この段階では、名前さえ合っていれば、どこにファイルを配置してもかまいません。

続いて作成した変換設定ファイルの中身を編集します。 今回は、このNuGetパッケージを参照した際、構成ファイルのAppSettingsセクションに要素を1つ追加するようにしてみます。 ということで、2つの変換設定ファイルを以下のように設定します。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="test-key" value="test-val"/>
  </appSettings>
</configuration>

このファイルには、このNuGetパッケージを動作させるために必要な設定を書いておくのがよいです。 例えばデータアクセスを行う必要があるなら、その接続文字列の設定例とかがそれにあたります。

追加した変換設定ファイルのビルドアクションを確認する

XMLファイルをプロジェクトに追加すると、通常ビルドアクションは「なし」になりますが、念のためちゃんと確認しておきましょう。 追加した変換設定ファイルを右クリックして[プロパティ]を選択します。

f:id:masatsuna:20190810234534p:plain
ビルドアクションの確認

これが「コンテンツ」になっていると、NuGetパッケージをビルドしたときまったく別の場所に配置されてしまう問題が発生します。 必ず「なし」になっていることを確認します。

*.nuspecファイルに作成した変換設定ファイルの設定を追加する

次に、追加した変換設定ファイルをNuGetパッケージ内に含めるよう、以下のように*.nuspecファイルを編集します。

<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <title>$title$</title>
    <authors>$author$</authors>
    <owners>$author$</owners>
    <licenseUrl>https://tsuna-can.hateblo.jp/</licenseUrl>
    <projectUrl>https://tsuna-can.hateblo.jp/</projectUrl>
    <iconUrl>https://tsuna-can.hateblo.jp/</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$description$</description>
    <releaseNotes>初版リリース。</releaseNotes>
    <copyright>$copyright$</copyright>
    <tags>サンプル</tags>
  </metadata>
  <files>
    <file src="ConfigTransform\*.transform" target="content" />
  </files>
</package>

以前の記事で作成した*.nuspecファイルに、files要素を追加しています。 files要素は、NuGetパッケージ内に含めるファイルを追加するための要素です。 ここでは、先ほど作成した変換設定のファイルのパスをワイルドカード文字を使って設定しています。 target属性には、src属性に指定したファイルをNuGetパッケージ内のどこに配置するかを指定します。

変換設定ファイルの配置場所は、NuGetの仕様でcontentディレクトリ内直下、と定められています。 そのため、target属性には「content」を指定し、そのディレクトリ内に変換設定ファイルが配置されるようにしているわけです。

ここまでで設定はすべて完了です。 一旦プロジェクトをReleaseに設定してビルドしておきます。

NuGetパッケージを作成する

続いて、NuGetパッケージの作成を行います。 前回とまったく同じコマンドで、NuGetパッケージを作成できます。

C:\XXXXX\AppSettings.Core>nuget pack -Properties Configuration=Release
'AppSettings.Core.csproj' からパッケージをビルドしています。
MSBuild auto-detection: using msbuild version '16.2.37902.0' from 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\bin'.
'C:\XXXXX\AppSettings.Core\bin\Release' のファイルをパックしています。
メタデータに 'AppSettings.Core.nuspec' を使用しています。
packages.config が見つかりました。依存関係として登録されているパッケージを使用します
Successfully created package 'C:\XXXXX\AppSettings.Core\AppSettings.Core.1.0.7163.30227.nupkg'.
警告: NU5125: The 'licenseUrl' element will be deprecated. Consider using the 'license' element instead.

特に警告に対する対処も行っていないので、同じ警告が出ています。 が、ここは無視して先に進みます。

NuGetパッケージの中身を確認する

では早速ビルドして出来上がった*.nupkgファイルの中身を確認してみます。 *.nupkgファイルはzip圧縮されているので、拡張子をzipに変更して展開すれば、中身を確認できます。 以下が今回作成した*.nupkgファイルを展開した中身です。

f:id:masatsuna:20190812171138p:plain
nupkgファイルの中身

前回は存在しなかったcontentディレクトリが作成されているのがわかります。 contentディレクトリ内を見てみると、以下のように今回追加した変換設定ファイルが含まれているのがわかります。

f:id:masatsuna:20190812171801p:plain
変換設定ファイル

NuGetパッケージとして追加して動作を確認する

最後に、作成したNuGetパッケージを実際に使って、動作確認を行ってみます。 適当にコンソールアプリケーションプロジェクトとWebアプリケーションプロジェクトを作成して、今回作成したNuGetパッケージを参照に追加してみます。 ローカル環境で動作確認するための方法は、前回記事を参考にしてください。

コンソールアプリケーション(App.config)の場合

Visual Studio.NET Frameworkのコンソールアプリケーションを作成すると、勝手にApp.configファイルが含まれていると思います。 私の環境では以下のようなApp.configファイルが自動的に生成されました。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
</configuration>

今回作成したNuGetパッケージを参照に追加すると、この構成ファイルが以下のように勝手に書き換わります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
  <appSettings>
    <add key="test-key" value="test-val" />
  </appSettings>
</configuration>

変換設定ファイルに記載した内容が構成ファイルに対して設定されており、ちゃんと変換設定が行われたことが確認できます。

今回はコンソールアプリケーションプロジェクトに対してNuGetパッケージを追加していますが、Windows Formsや、WPFなど、App.configを使用するプロジェクトでも同様の変換が実行されます。 またApp.configがプロジェクトに存在しない場合は、変換設定ファイルに記載した内容そのままのApp.configがプロジェクトに追加されます。

Webアプリケーション(Web.config)の場合

続いてWeb.configの変換がうまくできるかも確認してみます。 私の環境では、Visual Studioで空の.NET Framewoアプリケーション(.NET Framework)を作成すると、以下のようなWeb.configが生成されました。

<?xml version="1.0" encoding="utf-8"?>

<!--
  ASP.NET アプリケーションの構成方法の詳細については、
  https://go.microsoft.com/fwlink/?LinkId=169433 を参照してください
  -->
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.7.2"/>
    <httpRuntime targetFramework="4.7.2"/>
  </system.web>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs"
                type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb"
                type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>

</configuration>

今回作成したNuGetパッケージを参照に追加すると、この構成ファイルが以下のように勝手に書き換わります。

<?xml version="1.0" encoding="utf-8"?>

<!--
  ASP.NET アプリケーションの構成方法の詳細については、
  https://go.microsoft.com/fwlink/?LinkId=169433 を参照してください
  -->
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.7.2" />
    <httpRuntime targetFramework="4.7.2" />
  </system.web>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs"
                type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb"
                type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
                warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>

  <appSettings>
    <add key="test-key" value="test-val" />
  </appSettings>
</configuration>

こちらも変換設定ファイルに記載した内容が構成ファイルに対して設定されており、ちゃんと変換設定が行われたことが確認できます。

NuGetパッケージをアンインストールしたときの挙動

さて、ここまではインストール時に構成ファイルが正しく変換されることを確認してきましたが、今回作成したNuGetパッケージを削除したとき、どのような動作になるのでしょうか。 実は変換設定ファイルにある内容と一致するものについて、NuGetパッケージをアンインストールするとき、構成ファイルから削除してくれます。 例えば先の例で、コンソールアプリケーションのApp.configが書き換わる例について解説しましたが、このプロジェクトからNuGetパッケージをアンインストールすると、以下のように設定が削除されます。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>
  
</configuration>

変換設定ファイルと構成ファイルが完全一致する場合でも、NuGetパッケージのアンインストール時に、構成ファイル自体の削除は行われません。 変換設定ファイルと完全一致する構成ファイルの場合、以下のような空の構成ファイルが残されます。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  
</configuration>

また以下のように、変換設定ファイルの記載とずれがあるものについては、NuGetパッケージをアンインストールしても、設定は自動的に削除されません。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
  </startup>

  <appSettings>
    <add key="test-key" value="custom-val" />  <!-- value属性の値を変更している -->
  </appSettings>
</configuration>

こういった細かな制御まで行いたい場合は、XDT変換の機能を活用することで、より詳細な変換を実現することができます。

当ブログのロゴをフリーのロゴジェネレーターで作った話

私はデザインができない

私は思いっきり業務系に振り切ったエンジニアです。なのでお絵かきの能力とか、デザインセンスとかは全く実装されておりません!!

とはいっても、ブログをやるにあたって、何かサイトのロゴとかあったほうがいいよなーと思い、いろいろ調べて行きついたのがこちらのサービスでした。

hatchful.shopify.com

このサービスを使うと、自分のサイトの特徴とかを適当に選択して、サイトの名前とかを入れると、いい感じのロゴを大量に自動生成してくれるのです。なので自分でやったのは質問にいくつか答えて、生成された大量のロゴから好きなものを選んだだけです。めっちゃお手軽でした。

使い方

というわけで、このサービスはどんな感じで使えるのか、簡単にまとめてみます。2019/7/31時点の情報ですので、UIなど変わっていたらごめんなさい。

  1. まずは上記URLからサイトに飛び、[今すぐはじめる]を押下します。 f:id:masatsuna:20190731224104p:plain

  2. サービスやらビジネスやらサイトやら、自分の運営するもののカテゴリーを1つ選択します。画の都合で切れてますが、記事を書いている時点では13種類から選択できました。選択したら[次へ]を押下します。 f:id:masatsuna:20190731224251p:plain

  3. 次にロゴの持つスタイルを選択します。これは最大3つまで選択できるので、ブランドイメージに合ったものを選びましょう。選択したら[次へ]を押下します。 f:id:masatsuna:20190731224441p:plain

  4. 続いてロゴ内に含める文字列を設定します。ビジネス名には、通常はサイト名とか入れるんでしょうかね。スローガンもロゴに含まれますが、生成されるロゴによっては無視されることもあります。この辺は生成されたロゴを見ながら、行ったり来たりしていい感じのものを探していくのがよいと思います。また後から文字を変更することもできるので、迷ったら一旦未入力のままでも大丈夫です。ビジネス名、スローガンを入力したら[次へ]を押下します。 f:id:masatsuna:20190731224931p:plain

  5. 次にロゴを使うシーンを選択します。適当に使い道を選択してから[次へ]を押下します。 f:id:masatsuna:20190731225211p:plain

  6. これだけの操作で、大量にロゴのサンプルが出てきます。多分無限スクロールになっているので、自分の好きそうなデザインを頑張って探してみてください。ちなみに、色とかフォントとか、いろいろと後から変更できるので、まずはデザインから受けたイメージとか、印象優先で選択してしまえば良いと思います。デザインが決まったら、そのロゴを選択します。 f:id:masatsuna:20190731225432p:plain

  7. すると、デザインを調整する画面になります、ロゴに表示する文字列とか、色、フォントとかを調整できます。自由すぎるくらいいろいろ変えられるので、選択したデザインとは別物に変更することもできちゃいます。満足いくまでいじくりまわしてください。編集が完了したら[次へ]を押下します。 f:id:masatsuna:20190731225736p:plain

  8. 続いてダウンロード画面になるので、[ロゴをダウンロード]ボタンを押下します。 f:id:masatsuna:20190731230217p:plain

  9. ダウンロードするにはユーザー登録が必要なので、アカウントの作成等行ってダウンロードしてください。 f:id:masatsuna:20190731230242p:plain

まとめ

当ブログのロゴを非デザイナーである私がどうやって作ったのか、簡単にまとめてみました。おおよそ30分くらいで作れました。ロゴを生成するサービスは様々なものがあるので、ご自分にあったツールを探してみるのも楽しいかもしれませんね。

.NET FrameworkのNuGetパッケージを作成する

時代は.NET Core 3に入ろうとしていますが、懲りずに.NET FrameworkのNuGetパッケージの作り方をまとめます。

環境

  • .NET Framework 4.7.2
  • NuGet 5.1.0
  • Visual Studio (クラスライブラリプロジェクトを作る以外に使わないです。バージョンは適当に。)

今回作るもの

Visual Studioで作成したクラスライブラリプロジェクトをNuGetパッケージに変換して、他のプロジェクトから参照できるようにしたいと思います。作成するクラスライブラリは、無駄にJson.NETのNuGetパッケージを参照します。

なおググるといろいろなやり方が出てくるのですが、.NET FrameworkのNuGetパッケージを作るのであれば、この方法が一番おすすめです。

クラスライブラリの準備

必要なNuGetパッケージの参照を追加する

まずはクラスライブラリプロジェクトを作成し、ライブラリの実装に必要なNuGetパッケージを追加します。今回はJson.NETのNuGetパッケージと、StyleCop AnalyzersのNuGetを追加しておきます。なお今回これらのパッケージを追加しているのは、自作のNuGetパッケージを作成する際、参照しているNuGetパッケージがどのようにパッケージングされるかを説明する目的でしかありません。必要がなければ当然追加する必要ありませんのでご注意ください。

f:id:masatsuna:20190726001338p:plain
追加したNuGetパッケージ

正しく追加できると、クラスライブラリプロジェクトにpackage.configファイルが追加され、その中に追加したNuGetパッケージの情報が記録されます。今回は上記2つのNuGetパッケージを追加したので、以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
  <package id="StyleCop.Analyzers" version="1.1.118" targetFramework="net472" developmentDependency="true" />
</packages>

本校執筆時点で、様々な情報ソースを見ていると、package.configはオワコンで、PackageReferenceに移行しろ、という話をよく見かけます。ですが、.NET Framework上で動作するNuGetパッケージを作りたいのであれば、package.configのままにしておきましょう。

クラスライブラリを実装する

続いて適当にクラスを作成して、クラスライブラリプロジェクトに追加します。今回はNuGetパッケージ化して外部から呼び出すため、publicなクラスにしておきます。特に処理内容に意味はありません。突っ込み禁止。

namespace AppSettings.Core
{
    using System;
    using System.Configuration;
    using System.Linq;
    using Newtonsoft.Json;

    /// <summary>
    ///  AppConfigまたは環境変数の値を取得するための処理を提供します。
    /// </summary>
    public static class Settings
    {
        /// <summary>
        ///  指定したキーの情報を構成ファイルのAppSettingsまたは環境変数から取得します。
        ///  構成ファイルのAppSettingsに、指定したキーの情報が設定されている場合はその値を、
        ///  存在しない場合は環境変数の値を取得します。
        ///  AppSettingsにも環境変数にも指定したキーが存在しない場合、 <see langword="null"/> を返します。
        /// </summary>
        /// <param name="key">設定値を取得するキー。</param>
        /// <returns>設定値。存在しないキーを指定した場合 <see langword="null"/></returns>
        public static string Get(string key)
        {
            var appSettingValue = ConfigurationManager.AppSettings.Get(key);
            if (appSettingValue != null)
            {
                return appSettingValue;
            }

            var environmentValue = Environment.GetEnvironmentVariable(key);
            return environmentValue;
        }

        /// <summary>
        ///  構成ファイルのAppSettingsに設定されているキーと値を一覧をJSON形式の文字列で取得します。
        /// </summary>
        /// <returns>AppSettingsの設定値の一覧。</returns>
        public static string AppSettingsToJson()
        {
            var allItems = ConfigurationManager.AppSettings.AllKeys.ToDictionary<string, string, string>(key => key, key => Get(key));
            return JsonConvert.SerializeObject(allItems);
        }
    }
}

なおこんな形でXMLコメントを書いておくと、Visual StudioなどでこのAPIを参照したとき、インテリセンスに説明が出るようになります。ちゃんXMLを書いたら、プロジェクトのプロパティから[ビルド]タブを選択し、[構成]を[すべての構成]に設定してから、下部の[XML ドキュメント ファイル]にチェックを入れておきましょう。

f:id:masatsuna:20190727010042p:plain
XMLドキュメントファイルの設定

こうしておくと、インテリセンス用のXMLファイル(<DLLの名前>.xml)がビルド時に出力されるようになります。

自作NuGetパッケージのメタ情報を設定する

.NET Frameworkのプロジェクトを作成すると、AssemblyInfo.csというファイルが生成されます。

f:id:masatsuna:20190726003437p:plain
AssemblyInfo.csの場所

このファイルには、NuGetパッケージ化するにあたって必要な情報を設定しておきます。特に以下の属性値は、NuGetパッケージを作成する際使用しますので、必ず設定しておきましょう。

属性名 説明
AssemblyTitle NuGetパッケージの名前になります。DLLファイルの名前とそろえておくのをおすすめします。
AssemblyDescription NuGetパッケージの説明として使用します。日本語入力可能です。
AssemblyCompany NuGetパッケージの作者として使用します。通常は会社名を入れますが、個人名を入れてもかまいません。
AssemblyCopyright 著作権表記を入れます。
AssemblyVersion NuGetパッケージのバージョンとして使用します。プライベートなパッケージで、バージョン番号をビルドのたびに自動更新してほしい場合は、「1.0.*」のような形で指定してもかまいません。ただし、「1.0.0.*」はダメです。パッチバージョンに「*」を使ってください。「*」を使う場合は以下の追加設定が必要です。

設定が完了したら、ビルドを行っておいてください。

バージョン番号に「*」を使用する場合

割と新しめのVisual Studioを使用すると、AssemblyVersionに「*」を使用すると、以下のようなエラーが通知されます。

エラー CS8357 指定されたバージョン文字列には、決定性と互換性のないワイルドカードが含まれています。バージョン文字列からワイルドカードを削除するか、このコンパイルの決定性を無効にしてください。

これが出てしまったら、プロジェクトファイルを適当なエディタで手修正して、設定値を変更します。具体的には、プロジェクトファイルの「Deterministic」要素の値を「false」に設定します。

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{857E25AF-6880-42D2-937A-220BC2D15F11}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>AppSettings.Core</RootNamespace>
    <AssemblyName>AppSettings.Core</AssemblyName>
    <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <!-- 以下の設定値を変更する -->
    <Deterministic>false</Deterministic>
  </PropertyGroup>
  <!-- 以下略 -->

設定を変更したら、プロジェクトを再度読み込んでください。これで正常にビルドができればOKです。このとき、構成を「Release」にしてビルドしておいてください。構成の変更はVisual Studioツールバーから実行できます。

f:id:masatsuna:20190727000112p:plain
構成をReleaseに設定

この状態でビルドすると、「プロジェクトルート\bin\Release」にDLLとインテリセンス用のXMLが出力されます。

NuGetパッケージの作成

nuget.exeの入手とインストール

NuGetパッケージを作成するには、nuget.exeを使います。以下のページから「Windows x86 Commandline」のツールをダウンロードしてください。

NuGet Gallery | Downloads

ダウンロードしたら、適当なフォルダに配置し、そのフォルダへのパスを通しておきます。どうでもいいですけど、Windows 10になって環境変数の画面が改善されてすごいうれしいです。

以上でインストール完了です。

*.nuspecファイルの作成と設定

続いて、コマンドプロンプトを立ち上げて、先ほど作成したクラスライブラリのプロジェクトファイルのあるディレクトリに移動します。そこで、先ほどインストールしたnuget.exeを使って、NuGetパッケージの定義ファイルをである*.nuspecファイルを生成します。コマンドは以下の通りです。

C:\XXXXX\AppSettings.Core>nuget spec
'AppSettings.Core.nuspec' は正常に作成されました。

引数なしで実行すると、当該ディレクトリ内にあるプロジェクトファイルを探して、それを対象に*.nuspecファイルを生成してくれます。生成されたファイルは以下のような感じになっています。

<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <title>$title$</title>
    <authors>$author$</authors>
    <owners>$author$</owners>
    <licenseUrl>http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE</licenseUrl>
    <projectUrl>http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE</projectUrl>
    <iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$description$</description>
    <releaseNotes>Summary of changes made in this release of the package.</releaseNotes>
    <copyright>Copyright 2019</copyright>
    <tags>Tag1 Tag2</tags>
  </metadata>
</package>

この中で、「$~$」で囲われた値は、後でNuGetパッケージを作成するとき、パッケージ対象のアセンブリから値を拾ってきてくれるところです(正確には、パッケージ作成時にコマンドラインから引数として渡せる値です)。そうではない値が直接指定されている箇所は、適宜変更が必要ですので書き換えましょう。なお変数になっていない要素は、任意で設定できる要素なので、不要なら削除してしまってもかまいません。またcopyright要素は、「$copyright$」変数を使うとAssemblyInfo.csから設定値を引っ張ってこれますので、変数化しておくことをおすすめします。

なおnuspecファイルの各項目については、以下に詳細な解説があります。

NuGet の nuspec ファイルリファレンス | Microsoft Docs

ということで、今回は以下のような設定を行っておきました。

<?xml version="1.0"?>
<package >
  <metadata>
    <id>$id$</id>
    <version>$version$</version>
    <title>$title$</title>
    <authors>$author$</authors>
    <owners>$author$</owners>
    <licenseUrl>https://tsuna-can.hateblo.jp/</licenseUrl>
    <projectUrl>https://tsuna-can.hateblo.jp/</projectUrl>
    <iconUrl>https://tsuna-can.hateblo.jp/</iconUrl>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <description>$description$</description>
    <releaseNotes>初版リリース。</releaseNotes>
    <copyright>$copyright$</copyright>
    <tags>サンプル</tags>
  </metadata>
</package>

NuGetパッケージの作成

続いてプロジェクトファイルとnuspecファイルを組み合わせて、NuGetパッケージをビルドします。今回はReleaseモードでビルドするので、以下のようなコマンドを実行します。特にプロジェクトファイルやnuspecファイルを指定していませんが、この状態だとプロジェクトファイルを指定したのと同じ動作になります。

C:\XXXXX\AppSettings.Core>nuget pack -Properties Configuration=Release
'AppSettings.Core.csproj' からパッケージをビルドしています。
MSBuild auto-detection: using msbuild version '16.1.76.45076' from 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\bin'.
'C:\XXXXX\AppSettings.Core\bin\Release' のファイルをパックしています。
メタデータに 'AppSettings.Core.nuspec' を使用しています。
packages.config が見つかりました。依存関係として登録されているパッケージを使用します
Successfully created package 'C:\XXXXX\AppSettings.Core\AppSettings.Core.1.0.7147.102.nupkg'.
警告: NU5125: The 'licenseUrl' element will be deprecated. Consider using the 'license' element instead.

これでNuGetパッケージ本体のnupkgファイルを生成することができました。

なおnuget packを実行すると、上記のように警告が出る場合があります。警告が出ても、出力されたNuGetパッケージは使用することができますが、適宜修正を入れることをおすすめします。

出力されたnupackファイルを確認する

NuGetパッケージ本体の*.nupkgファイルですが、こいつはzip形式で圧縮されています。ですので、拡張子をzipに変更すれば、普通に展開して以下のように中身を確認することができます。

f:id:masatsuna:20190727001400p:plain
nupkgファイルの中身

この中で最も重要なのは、libディレクトリです。この中身をのぞいてみると、今回実装したクラスライブラリのDLLと、インテリセンス用のXMLが配置されているのが確認できると思います。今回は.NET Framework 4.7.2をターゲットに作成しているので、net472ディレクトリ内にファイルが配置されています。Visual StudioでNuGetパッケージを参照すると、物理的にはこのDLLが使われることになります。またインテリセンス用のXMLを出力しておくと、ちゃんとNuGetパッケージの中にもインテリセンス用のXMLが同梱されます。

f:id:masatsuna:20190727011017p:plain
libディレクトリの中身

続いて大切なのは、ルートディレクトリにある.nuspecファイルです。この前の作業で、.nuspecファイルを生成し、設定を書き換えたと思います。ここに出力されたファイルは、手修正したnuspecファイルに対して、NuGetパッケージを作成する対象のプロジェクトファイルや、DLL、package.configファイルの情報を追記した完成形のファイルになっています。今回作成したnupkg内のnuspecファイルは、以下のようになっています。

<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
  <metadata>
    <id>AppSettings.Core</id>
    <version>1.0.7147.102</version>
    <title>AppSettings.Core</title>
    <authors>Tsuna-can</authors>
    <owners>Tsuna-can</owners>
    <requireLicenseAcceptance>false</requireLicenseAcceptance>
    <licenseUrl>https://tsuna-can.hateblo.jp/</licenseUrl>
    <projectUrl>https://tsuna-can.hateblo.jp/</projectUrl>
    <iconUrl>https://tsuna-can.hateblo.jp/</iconUrl>
    <description>デモ用のパッケージです。</description>
    <releaseNotes>初版リリース。</releaseNotes>
    <copyright>Copyright ©  2019</copyright>
    <tags>サンプル</tags>
    <dependencies>
      <dependency id="Newtonsoft.Json" version="12.0.2" />
    </dependencies>
  </metadata>
</package>

もともと作成していた「*.nuspec」ファイルには、「$~$」で指定した変数が随所に書かれていたと思います。最終的に出力されたnuspecファイルには、AssemblyInfo.csに実装した値が書き込まれているのがわかります。以下が今回使用したAssemblyInfo.csファイルです。

using System.Reflection;
using System.Runtime.InteropServices;

[assembly: AssemblyTitle("AppSettings.Core")]
[assembly: AssemblyDescription("デモ用のパッケージです。")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Tsuna-can")]
[assembly: AssemblyProduct("AppSettings.Core")]
[assembly: AssemblyCopyright("Copyright ©  2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("857e25af-6880-42d2-937a-220bc2d15f11")]
[assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyFileVersion("1.0.0.0")]

今回はアセンブリバージョンに「1.0.」をを指定しているので、ビルド時にバージョン番号が採番され、その値がversionタグに入っているのがわかります。またもともと作成していた「.nuspec」ファイルに変数で指定しなかった箇所については、その値がそのまま出力されています。

もう1点、もともと作成していた「*.nuspec」ファイルとは異なる箇所があります。それがdependencies要素です。実はこの要素は、package.configに設定している内容が書き込まれるようになっています。今回、package.configには以下2つのNuGetパッケージが含まれている状態でした。

  • Newtonsoft.Json
  • StyleCop.Analyzers

今回出力されたnuspecファイルには、Newtonsoft.Jsonの情報のみが記録されています。なぜこのような動作をするかというと、package.configのStyleCop.Analyzersの設定で、developmentDependency属性がtrueに設定されていることが影響しています。developmentDependency属性がtrueに設定されているNuGetパッケージは、そのプロジェクトを開発するときにだけ使用するNuGetパッケージであることを示しており、nuget packしたとき、dependenciesに追加されません。StyleCop.Analyzersは、静的コード分析を行うためのパッケージであるため、今回作成したNuGetパッケージを使う人にとって不要だ、ということですね。静的コード分析を行うためのNuGetパッケージは割と流通しているのですが、中にはNuGetパッケージの追加を行ったとき、developmentDependency属性を設定してくれないものがありますので注意してください。

動作確認

ローカルディレクトリをパッケージソースとして追加する

このようにして作成したNuGetパッケージですが、本当に正しく参照できるのか、ローカル環境で確認してみます。

まずVisual Studioを開き、[ツール]メニューの[オプション]を選択します。左側ペインで[NuGet パッケージ マネージャー]→[パッケージ ソース]を選択します。初期状態だと以下のような設定になっていると思います。

f:id:masatsuna:20190727004317p:plain
パッケージソース

ここに、今回作成したnupkgファイルのあるパスを追加することで、ローカルPC内のNuGetパッケージとして参照できるようになります。右上の「+」ボタンを押下して、新しく行を追加してください。追加した行を選択し、下部の[名前]に任意の名前を、[ソース]に作成したnupkgファイルがあるディレクトリを入力して[更新]ボタンを押下します。

f:id:masatsuna:20190727004700p:plain
ローカルディレクトリをパッケージソースに追加

私は今回、作成したnupkgファイルを「C:\nuget」ディレクトリに移動して動作確認を行っています。そのためこのような設定になっています。最後に[OK]ボタンを押下します。

NuGetパッケージとしてプロジェクトから参照する

続いて、作成したNuGetパッケージを参照するプロジェクトを適当に作成し、NuGetパッケージの追加を行います。その際、右上にある[パッケージ ソース]のドロップダウンから、先ほど追加したパッケージソースを選択します。これで、作成したNuGetパッケージがいつものあの画面に出てくると思います。また、nupkgファイル内に含まれているnuspecファイルに設定した内容が、画面に出力されているのが確認できると思います。

f:id:masatsuna:20190727005121p:plain
自作のNuGetパッケージを追加する

あとは通常通り、バージョンを選択して[インストール]を押下すると、NuGetパッケージがインストールされます。インストールが完了すると、package.configも更新されます。また依存関係にあるNuGetパッケージも、ちゃんと同時に追加されます。

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="AppSettings.Core" version="1.0.7147.102" targetFramework="net472" />
  <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
</packages>

今回のまとめ

今回は.NET Framework上で動作するNuGetパッケージの作成方法をまとめてきました。今回はnupkgファイルを作成してローカルマシン内で動作確認を行うまでの手順を書いてきましたが、実際にはどこか参照できるところにアップロードして、初めてみんなが使える状態になります。今回その手順の説明が入っていないので注意してください。

Entity Framework 6で実行したSQLと実行時間をアプリケーション内で取得する方法

Entity Frameworkを使用すると、特にSQLを意識しなくても、データアクセス処理を実装することができてしまいます。その反面、実際にどのようなクエリが流れているのか、そしてそのクエリの実行にどのくらいの時間がかかっているのかを後から知ろうとすると、SQL Profilerなどを使ってデータベース側で監視する必要があります。これをもうちょっとお手軽にアプリケーション側で測定する方法を書いてみます。

環境

Entity Frameworkのインターセプターを実装する

Entity Frameworkには、SQLクエリを実行する前後に処理を差し込むためのインターセプターという仕組みが存在します。この仕組みを用いることで、実際に実行したクエリや、その実行時間を記録することができるようになります。インターセプターは、System.Data.Entity.Infrastructure.Interception.IDbCommandInterceptor を実装して、アプリケーションコードまたは構成ファイルを通して適用することで利用できるようになります。今回は構成ファイルを用いて適用する方法を書いてみます。

さて、IDbCommandInterceptorは、以下のようなインターフェースを持っています。

public interface IDbCommandInterceptor : IDbInterceptor
{
    void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext);
    void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext);
    void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext);
    void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext);
    void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext);
    void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext);
}

XXingという名前のメソッドが、SQLクエリを実行する前に差し込まれる処理で、XXedという名前のメソッドが、SQLクエリが実行された後に差し込まれる処理です。実行するSQLクエリの種類に応じて、3つのペアのうちどれかが実行されます。

  • NonQueryExecute系
    • DDLの実行やINSERT, UPDATE, DELETEなどの処理を実行したときに呼び出されます
    • ただし、INSERT, UPDATE, DELETEを行った後SELECTが内部的に実行される場合は、ReaderExecute系が動作します
  • ReaderExecute系
    • データリーダーを用いるSELECT処理を実行したときに呼び出されます
    • 表形式のデータを取得する処理で呼び出されます
  • ScalarExecute系
    • COUNTなどの集計関数を用いるSELECT処理を実行したときに呼び出されます

上記の前提を踏まえて、IDbCommandInterceptorを実装したクラスに対して、ログ出力処理を組み込んでみます。今回はことを簡単にするため、以下の条件で実装していますので、コピペする際はご注意ください。

  • 例外処理はそれほど考えてありませんし、条件によっては動作しないケースがあるかもしれません
  • ログ出力は処理が完了したタイミングで、Traceに対して実行します
  • ログには以下の項目を含めます
    • 実行したSQLクエリ
    • そのクエリに対してアプリケーションから与えたパラメーター
    • SQLクエリの実行に要した時間
    • 例外が発生してる場合、その例外の情報

この条件で、実装すると以下のようになります。

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Diagnostics;

namespace SampleClassLib.Data
{
    public class DBLogInterceptor : IDbCommandInterceptor
    {
        private static readonly ConcurrentDictionary<DbCommand, Stopwatch> StopwatchList = new ConcurrentDictionary<DbCommand, Stopwatch>();

        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            long? elapsedTime = StopStopWatch(command);
            WriteLog(command, elapsedTime, interceptionContext.Exception);
        }

        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            StartStopWatch(command);
        }

        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            long? elapsedTime = StopStopWatch(command);
            WriteLog(command, elapsedTime, interceptionContext.Exception);
        }

        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            StartStopWatch(command);
        }

        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            long? elapsedTime = StopStopWatch(command);
            WriteLog(command, elapsedTime, interceptionContext.Exception);
        }

        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            StartStopWatch(command);
        }

        private static void WriteLog(DbCommand command, long? elapsedTime, Exception ex)
        {
            List<string> parameters = new List<string>();
            foreach (DbParameter parameter in command.Parameters)
            {
                parameters.Add($"\"{parameter.ParameterName}\":\"{parameter.Value}\"");
            }

            if (ex != null)
            {
                Trace.WriteLine($"【Error!!】実行時間:{elapsedTime}ms, 実行コマンド:{command.CommandText}, パラメーター:{{{string.Join(",", parameters)}}}, 例外情報:{ex.ToString()}");
            }
            else
            {
                Trace.WriteLine($"実行時間:{elapsedTime}ms, 実行コマンド:{command.CommandText}, パラメーター:{{{string.Join(",", parameters)}}}");
            }
        }

        private void StartStopWatch(DbCommand command)
        {
            if (command == null)
            {
                return;
            }

            Stopwatch stopwatch = Stopwatch.StartNew();
            StopwatchList.TryAdd(command, stopwatch);
        }

        private long? StopStopWatch(DbCommand command)
        {
            if (command == null)
            {
                return null;
            }

            if (StopwatchList.TryRemove(command, out Stopwatch stopwatch))
            {
                stopwatch.Stop();
                return stopwatch.ElapsedMilliseconds;
            }

            return null;
        }
    }
}

なお今回は、このクラスをログ出力させたい本体のアプリケーションとは、まったく別のクラスライブラリプロジェクトに実装しておきましょう。このクラスを含むクラスライブラリを単体でビルドして、DLLを作っておきましょう。これで準備が整いました。

アプリケーション本体への組み込み

作成したDLLを、ログ出力を行いたいアプリケーション本体に組み込むには、アプリケーションの実行ディレクトリに、作成したDLLを配置し、構成ファイルに設定を記載することになります。本体のアプリケーションの設定変更や、参照の追加などは必要ありません。

Webアプリケーションの場合は、ルートディレクトリの直下にあるbinディレクトリ内に、コンソールアプリケーションなどの実行ファイルを持つアプリケーションの場合は、EXEファイルと同じディレクトリ内にビルドしたDLLを配置します。

f:id:masatsuna:20190717231502p:plain
Webアプリケーションの場合

f:id:masatsuna:20190717231649p:plain
実行ファイルを持つアプリケーションの場合

今回作成したDLLは「SampleClassLib.Data.dll」という名前のDLLです。これを、各実行ディレクトリに配置すると、上記のようになります。

構成ファイルの設定

続いて、構成ファイルの設定を変更していきます。Webアプリケーションの場合は、ルートディレクトリにあるWeb.configを、実行ファイルを持つアプリケーションの場合は、実行ディレクトリ内に存在する「<実行ファイルの名前>.exe.config」が構成ファイルになります。これらのファイルをエディターで開き、設定を追加します。

今回作成したインターセプターは、System.Diagnostics.Trace クラスを用いてログ出力を行うよう実装しています。ですので、まずはTraceクラスが使用するトレースリスナーの設定を行います。Traceクラスにトレースリスナーを登録するには、構成ファイルの<system.diagnostics>セクション<trace>/<listeners>要素に<add>要素を追加して、トレースリスナーの型と、その設定を記載します。例えばファイル出力を行うための設定例は以下のようになります。この状態でTraceクラスを呼び出すと、ルートディレクトリにDataAccess.logという名前のログファイルが出力され、ログが記録されるようになります。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!-- 他の設定値は省略 -->
  <system.diagnostics>
    <trace autoflush="true">
      <listeners>
        <add name="file" type="System.Diagnostics.TextWriterTraceListener" initializeData="DataAccess.log"  />
      </listeners>
    </trace>
  </system.diagnostics>
</configuration>

なおトレースリスナーは既定でいくつか別の実装もあります。今回はファイル出力する例を用いていますが、コンソール画面に出力させたり、イベントログに出力させたりすることもできます。またトレースリスナーを複数登録すると、同じログを複数の場所に出力することもできます。.NET Frameworkに標準で準備されているトレースリスナーは、以下を参照してください。TraceListenerクラスの派生クラスが、使用可能なトレースリスナーです。

docs.microsoft.com

続いて、作成したインターセプターを登録します。インターセプターは、<entityFramework>セクション<interceptors>要素に<interceptor>要素を追加して、作成したインターセプターの型を記載します。今回はSampleClassLib.Data.dllにあるSampleClassLib.Data.DBLogInterceptorクラスを登録するので、以下のように設定します。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <configSections>
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
  <!-- 他の設定値は省略 -->
  <entityFramework>
    <!-- 他の設定値は省略 -->
    <interceptors>
      <interceptor type="SampleClassLib.Data.DBLogInterceptor, SampleClassLib.Data"></interceptor>
    </interceptors>
  </entityFramework>
</configuration>

このようにすることで、Entity Frameworkを用いてデータアクセスを行うと、作成したインターセプターが動作して、ログ出力が行われます。

まとめ

今回はEntity Frameworkのインターセプターという仕組みを用いて、Entity Frameworkを通して実行したSQLクエリと、その実行時間をログ出力する方法を書きました。今回紹介した方法だと、本体のアプリケーションコードを変更することなく、後からログ出力機能をわずかな設定変更のみで適用することができます。またログ出力を行いたいプロジェクトから、インターセプターを実装したクラスが含まれるDLLに対して参照を追加する必要もありません。コード修正ができないケースでも使える方法ですので、参考にしてみてください。

SendGridで送信遅延が発生したときの挙動

SendGridとは

メール送信を行うためのクラウドベースのサービスです。Azureサブスクリプション (Visual Studioサブスクリプションを含みます) を持っている場合、Azure PortalからSendGridのアカウントを作成することができます。Azure Portalには2019/7/7現在、以下のような説明が記載されています。

SendGrid is the world's largest cloud-based service for delivering email that matters. SendGrid's proven platform successfully delivers over 18B transactional and marketing related emails each month for Internet and mobile-based customers like Airbnb, Pandora, Hubspot, Spotify, Uber and FourSquare as well as more traditional enterprises like Walmart, Intuit and Costco. Azure customers receive up to 25,000 emails per month for free with paid packages starting at only $9.95 per month. For customers requiring ability to send larger email volumes, SendGrid also offers Silver, Gold, Platinum and Premier packages, which include a dedicated IP as well as additional IP and user management features.

月間25,000通までは無料でメール送信ができてしまうというお手軽さです。最近Azureでメール送信となるとほぼこれを使っています。ちなみにAzureサブスクリプションと紐づけてSendGridを使用する場合、いろいろ制限があります。詳細は以下のリンク先を参照してください。 blogs.msdn.microsoft.com

また課金体系についてはこちらが参考になります。 azuremarketplace.microsoft.com

送信遅延が発生

さて、本題です。先日私が運用しているSendGridアカウントにて、送信遅延が発生しました。送信遅延が発生すると、SendGridのポータルや、Web APIを経由して、送信遅延が発生していたことを知ることができます。ちなみにSendGridのWeb APIを使用する場合、Global Statsを使用することで、送信遅延の有無を知ることができます。

送信履歴の集計結果をC#プログラムで取得する

サクッと確認するなら素のWeb APIをたたいたり、SendGridのポータルを確認してもいいのですが、C#から取得したいならSendGridのNuGetパッケージを使うのもアリです。

今回は.NET CoreのコンソールアプリケーションからSendGridのWeb APIを叩いてみます。まずは.NET CoreのコンソールアプリケーションプロジェクトをVisual Studioで生成します。作成したプロジェクトに対して、[Sendgrid] という名前のNuGetパッケージを追加します。

f:id:masatsuna:20190707235908p:plain
Sendgridパッケージの追加

そして、以下のようにコードを書きます。

using System;
using SendGrid;

namespace SendGridClientTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Run().Wait();
        }

        private static async System.Threading.Tasks.Task Run()
        {
            string apiKey = "<SendGridのポータルで生成したAPIキー>";
            var client = new SendGridClient(apiKey);
            string queryParams = @"{
                'aggregated_by': 'day', 
                'end_date': '2019-06-29', 
                'limit': 1, 
                'offset': 1, 
                'start_date': '2019-06-29'
            }";
            var response = await client.RequestAsync(method: SendGridClient.Method.GET, urlPath: "stats", queryParams: queryParams);
            string responseStr = await response.Body.ReadAsStringAsync();
            Console.WriteLine(response.StatusCode);
            Console.WriteLine(responseStr);
        }
    }
}

これを実行すると、コンソール画面に指定した日のメール送信結果を集計した内容が表示されます。

f:id:masatsuna:20190708000745p:plain
実行結果

ちょっと結果が読みにくいので、JSON部分をフォーマットすると以下のようになります。

[
  {
    "date": "2019-06-29",
    "stats": [
      {
        "metrics": {
          "blocks": 0,
          "bounce_drops": 0,
          "bounces": 0,
          "clicks": 0,
          "deferred": 15,
          "delivered": 10,
          "invalid_emails": 0,
          "opens": 1,
          "processed": 10,
          "requests": 10,
          "spam_report_drops": 0,
          "spam_reports": 0,
          "unique_clicks": 0,
          "unique_opens": 1,
          "unsubscribe_drops": 0,
          "unsubscribes": 0
        }
      }
    ]
  }
]

送信遅延があったことを表す "deferred" というプロパティに15が記録されています。これは送信遅延が15回あったことの証拠になります。

送信遅延時の動作

さて、続いてSendGridのポータル画面から、送信遅延時にSendGridがどのような動作をしているのか確認してみます。

まず前提として、SendGridの仕様に関するページ*1を参照すると、以下のような記載があります。

Deferredが発生した場合、SendGridは時間を置いて再送信を繰り返します(72時間)。

では実際の動作はどうなっているのか、SendGridのポータル画面から確認します。SendGridの詳細ログは、[Activity] メニューから確認することができます。送信履歴の中から、[TYPE] 列が "deferred" となっているレコードを探します。

f:id:masatsuna:20190714233536p:plain
送信遅延時の挙動

今回のケースでは、5名に対するメール送信を同時に実行しています。それがそれぞれ3回ずつ遅延し、結果として15回の送信遅延が発生していた様子がわかりました。SendGridのログを見る限り、メール送信のリクエストを受け付けてから、 "deferred" が記録されるまでの間隔はおおよそ10分間のようです。この間実際に何回の再送処理が行われているのか、ログから正確なことを判断することはできませんが、少なくともこのログが記録されている瞬間には、再送を行っていたと考えられます。

ただ、根本的な問題がどこにあったのか、SendGridのポータルから確認することはできません。わかるのは、あくまで送信遅延があったという事実のみで、その原因が何なのかを知ることはできません。

なお、SendGridのポータルのダッシュボードに表示されているメール送信の成功率は、送信遅延による送信失敗はカウントされません。送信遅延が発生したとしても、最終的にメールが届いていれば、送信成功としてカウントされます。

メール送信時の挙動

ではメールを送信したアプリケーション側ではどのような挙動を示すのか、その答えはSendGridのメール送信用Web APIに答えがあります。メール送信用Web APIの解説の最下部に、HTTPリクエストの応答メッセージ例が示されています。

Response Code: 202 Accepted

そう、HTTPステータスコードは202 Acceptedなんですね。202は、要求を受け付けたけものの、まだ処理をしていないことを示すステータスコードです。

RFC 7231 - Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content

結論、アプリケーション側にはエラーやタイムアウト、遅延している、といった情報は一切通知されず、正常に処理を受け付けたことだけが返されます。

まとめ

今回はSendGridの送信遅延が発生したときの内部動作について書いてみました。ちょっと厄介なのは、送信遅延が発生したとき、SendGridが72時間も再送を繰り返す仕様です。通常メールの不達が発生したら、アプリケーション側である程度制御したいケースというのはあると思うのですが、それが許されないのはちょっとだけ厄介ですね。

Visual Studio 2019でAzure Functionsのテンプレートを使った時の注意点

問題点

Visual Studio 2019でAzure Functionsのテンプレートを使用すると、作成されるファイルのエンコードANSI(Shift-JIS)になります。普通に使っている分には困らないと思いますが、Azure DevOpsとかにそのままファイルを上げてしまうと、ブラウザから参照したとき、日本語部分がもれなく文字化けします。

環境

  • Visual Studio 2019 (16.1.5)
  • Azure Functions v2 (.NET Core) テンプレートを使用

Azure Functionsのアプリケーションを作成する

Visual Studio 2019を起動して、新しくAzure Functionsのアプリケーションを作成します。

f:id:masatsuna:20190706235651p:plain
Azure Functionsプロジェクトの作成

今回はAzure Functions v2 (.NET Core) のテンプレートを用います。またトリガーとかストレージアカウントとかは適当に選択します。

f:id:masatsuna:20190706235829p:plain
新しい Azure Functions アプリケーションの作成

生成されたファイルの文字コードを確認

すると、Function1.csというファイルが生成されます。実際に文字コードがどうなっているのか確認してみます。

まずFunction1.csファイルを開いて、Visual Studioの [ファイル] メニューから、[名前を付けて Function1.cs を保存] を選択します。

f:id:masatsuna:20190707000101p:plain
名前を付けて保存

[名前を付けてファイルを保存] のダイアログが開くので、[上書き保存] ボタンの右側にあるドロップダウンを開き、[エンコード付きで保存] を選択します。

f:id:masatsuna:20190707000218p:plain
エンコード付きで保存

すると、そのファイルがどの文字コードで保存されているかを確認することができます。図の通り、既定ではANSI (Shift-JIS) で保存されていることがわかります。

f:id:masatsuna:20190707000321p:plain
既定の文字コードを確認

文字コードを変更する

そのまま [エンコード] のドロップダウンから、[Unicode (UTF-8 シグネチャなし) - コードページ 65001] を選択することでBOMなしUTF-8に変更することができます。BOMありにする場合はシグネチャありのほうを選択してください。

f:id:masatsuna:20190707003427p:plain
BOMなしUTF-8に変更

新しくAzure Functionsのクラスファイルを追加した場合

プロジェクトに対して新しくAzure Functionsのクラスファイルを追加した場合も、同様に文字コードANSIになります。

f:id:masatsuna:20190707004238p:plain
新しくFunctionを追加(英語名)
f:id:masatsuna:20190707004316p:plain
文字コードANSI

ただし、追加するファイルの名前に日本語を使用した場合、文字コードはBOMなしUTF-8になります。

f:id:masatsuna:20190707004410p:plain
新しくFunctionを追加(日本語名)
f:id:masatsuna:20190707004436p:plain
文字コードはBOMなしUTF-8

プロジェクトファイルの文字コード

プロジェクトファイルは、ANSI (Shift-JIS) となります*1

f:id:masatsuna:20190707004654p:plain
プロジェクトファイルの文字コードANSI

他のエディターで無理やり文字コードを変更することはできますが、Visual Studioで変更を加えると、ANSIに戻ってしまいます。プロジェクトファイルの文字コード変更は今 (2019/7/7) のところ諦めるしかないかもしれません。よくわからない謎仕様ですね。UTF-8にそろえるようフィードバックを出しておきました。


2019/07/16 追記 この件バグとして取り扱ってもらえたようです。Azure FunctionsのGitHubにバグとして取り込まれました。詳細は以下より参照ください。

github.com

*1:サクラエディターで確認しています。

Entity Framework 6 DBファーストで数値型のカラムをEnumとして取り扱う方法

やりたいこと

Entity Frameworkのデータベースファーストを使用する場合、モデルクラスの各プロパティの型は、データベースの型によって勝手に決められます。「XXステータス」のように、取りうる値が決まっているケースでは、.NETアプリケーション内でできればEnumを使用したいところです。しかし、単純にモデルクラスを生成しただけでは、Enumを使うようにしてくれません。今回はEnumを使うように設定する方法を解説します。

環境

対象のテーブル

以下のようなテーブルを準備します。

CREATE TABLE [dbo].[Table] (
    [Id]     INT      NOT NULL,
    [Status] SMALLINT NOT NULL
);

とりあえずモデルクラスを生成する

Visual Studioで「ADO.NET Entity Data Model」を新たに作成します。そして上記のテーブルをモデルとして生成します。すると以下のようなモデルが生成されます。

f:id:masatsuna:20190702111934p:plain
Entity Framework 6のデザイナー

「Status」のプロパティを参照すると、型が「Int16」にマッピングされているのがわかります。次にこの「Status」をEnumで取り扱えるようにします。

Enumを使えるようにする

まずedmxファイルを開いて、対象のプロパティを右クリックします。そして「Enumに変換」を押下します。

f:id:masatsuna:20190702112904p:plain
Enumに変換

新たに列挙型を作成する場合

新たに列挙型を作成する場合は、「列挙型の追加」画面で追加する列挙型の名前、基になる型、値のリストを設定して「OK」ボタンを押下します。

f:id:masatsuna:20190702113537p:plain
Enumの設定

すると、設定した通りのEnumが新たに生成されます。生成する先は、以下の通りです。

f:id:masatsuna:20190702113927p:plain
Enumの生成後

なお設定したのにEnumが生成されいていない場合は、再度ビルドするか、edmxを右クリックして「カスタムツールの実行」を選択してください。

f:id:masatsuna:20190702114139p:plain
カスタムツールの実行

生成されたEnumは以下のようになります。

//------------------------------------------------------------------------------
// <auto-generated>
//     このコードはテンプレートから生成されました。
//
//     このファイルを手動で変更すると、アプリケーションで予期しない動作が発生する可能性があります。
//     このファイルに対する手動の変更は、コードが再生成されると上書きされます。
// </auto-generated>
//------------------------------------------------------------------------------

namespace EF_Enum.DataAccess
{
    using System;
    
    public enum Status : short
    {
        Default = 0,
        Executing = 1,
        Done = 2
    }
}

既存の列挙型を使用する場合

既に列挙型の定義ができている場合は、その型をモデルクラスで使用することもできます。以下のEnumをEntity Frameworkのモデルクラスから使用してみます。

namespace EF_Enum
{
    public enum Status : short
    {
        Default = 0,
        Executing = 1,
        Done = 2
    }
}

前述の手順同様、「列挙型の追加」画面まで進みます。「名前」に任意の名前を入れて、「外部の型の参照」に存在するEnumの型名を設定します。型名は名前空間を含む完全修飾名で指定することに注意してください。

f:id:masatsuna:20190702120649p:plain
既存のEnumを利用する場合

モデルクラスの再生成を行うと、モデルクラスが以下のように書き換わります。

//------------------------------------------------------------------------------
// <auto-generated>
//     このコードはテンプレートから生成されました。
//
//     このファイルを手動で変更すると、アプリケーションで予期しない動作が発生する可能性があります。
//     このファイルに対する手動の変更は、コードが再生成されると上書きされます。
// </auto-generated>
//------------------------------------------------------------------------------

namespace EF_Enum.DataAccess
{
    using System;
    using System.Collections.Generic;
    
    public partial class Table
    {
        public int Id { get; set; }
        public EF_Enum.Status Status { get; set; }
    }
}

確かに、指定したEnumが使われるようになっていることがわかります。

何が嬉しいのか

特にWhere句を組み立てる際、以下のように型を利用した実装ができるようになります。実装から「妙な数字」が消え去るので、可読性が大きく向上します。

using (EFEnumTestDbContext context = new EFEnumTestDbContext())
{
    var count = context.Tables.Where(r => r.Status == Status.Done).Count();
}

Azure DevOpsで監査ログを参照できるようになる話

監査ログって

システムにおける監査ログとは、ざっくり言えばシステムに対して「誰が何をしたのか」を記録したものです。以前までAzure DevOpsには、もっと全般的な使われ方を記録してくれる機能(Usage)はあったのですが、監査ログとして使うのには、内容が細かすぎるのと、本当に大切なことがどれなのか判断することが難しいという根本的な課題がありました。Azure DevOpsをエンタープライズ向けのお堅いシステム開発で使おうとしたとき、監査ログがないと採用できないケースというのがあったのですが、今回のアップデートで監査ログの取得ができるようになっています(2019/6/30時点ではプレビュー)。

どこから参照するの?

Azure DevOpsの監査ログは、設定画面から確認できます。まず組織のトップ画面左下にある「Organization Settings」を開きます。

Organization Settings
Organization Settings

次に「Auditing」のメニューを開きます。

Auditing
Auditing

そうすると、だれがいつ、何をしたかの監査ログを参照することができます。またビューに表示するログの期間を選択することができるようになっています。

Auditing log items
Auditing log items

現在のところ、最も古いデータが2019年5月下旬ごろのデータから確認できました。これより古いデータは表示できないようです。またログはダウンロードすることもできます。右上の「Download」ボタンを押下すると、保存形式を選択できます。

Select format
Select format

現在のところCSV形式、JSON形式が選択できます。なおこのボタンでダウンロードできるのは、ビューで選択した期間のものだけのようです。

REST API

Azure DevOps REST APIのバージョン5.1(現在プレビュー中)から、監査ログを取得するためのAPIが用意されています。Azure DevOps自体がどのくらいの期間監査ログを保存してくれるか不明 (2019/7/9 ログの保存期間は90日でした*1。情報提供ありがとうございます。) ですので、心配な場合はこのAPIを使ってバックアップを取っておく必要があるかもしれませんね。 docs.microsoft.com