ツナ缶雑記

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

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