ツナ缶雑記

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

構成ファイルの書き換えを行う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を眺めたのがきっかけです。まさか公式ドキュメントに書かれていないものがあるとは発想すらなかった。。。