NuGetパッケージが不要なnetstandardパッケージをずるずると追加する問題

Xamarin.AndroidやXamarin.iOSでNuGetパッケージを追加していると、たまに膨大な.NET Standardのパッケージが追加されて「は?」ってなることがあると思います。具体的にはNewtonsoft.Jsonとか。

無駄に縦に長いNewtonsoft.Jsonの依存パッケージリスト

こいつら実のところゴミなんです。いらないんです。しかもゴミなのに消せないんです。Newtonsoft.Jsonが依存しているからちゃんと消せないようなかたちで追加してあげたのねん! ボンビー!

これが原因で、ビルド時になると、MSBuildがこんな警告を出してくるようになります(原典はxamarin-androidのgithub issue)。

2> "Microsoft.CSharp, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" was chosen because it was primary and "Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" was not.
2> References which depend on "Microsoft.CSharp, Version=2.0.5.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" [C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\ReferenceAssemblies\Microsoft\Framework\MonoAndroid\v1.0\Microsoft.CSharp.dll].
2> C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\ReferenceAssemblies\Microsoft\Framework\MonoAndroid\v1.0\Microsoft.CSharp.dll
2> Project file item includes which caused reference "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\IDE\ReferenceAssemblies\Microsoft\Framework\MonoAndroid\v1.0\Microsoft.CSharp.dll".
2> Microsoft.CSharp
2> References which depend on "Microsoft.CSharp, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" [].
2> C:\Users\mml\Development\test_app\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll
2> Project file item includes which caused reference "C:\Users\mml\Development\test_app\packages\Newtonsoft.Json.10.0.3\lib\netstandard1.3\Newtonsoft.Json.dll".
2> C:\Users\mml\Development\test_app\tes_app\bin\Debug\netstandard1.6\test_app.dll
2> Newtonsoft.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL

何だそりゃ、ってなりますよね。これは全てNuGetの出来が悪いせいです。NuGetがやっつけ仕様で出来ているためです。正確に言えば、NuGetの実装が悪いわけではなくて、NuGetで運用されるパッケージ管理システムがきちんと設計されていないことに起因します。

Newtonsoft.Jsonのnupkgには、さまざまなプロファイルのアセンブリが含まれています。

無駄に横に長いNewtonsoft.Jsonのプロファイルリスト

Xamarin.Androidのプロジェクトにこれが追加された時に、実際にプロジェクトで利用されるプロファイルはどれでしょうか? netstandard1.3? portable-net45+win8+wp8+wpa81 (Profile259) ?

これ、実際にパッケージ解決に使われているのは、netstandard1.3なんです。ちなみにここでProfile259に解決されていれば、この問題は生じなかったんですね。Profile259には依存関係の列挙が無いのです。

ところでこの「正解」はどうすれば選べるのでしょうか? NuGetパッケージの開発者には、Profile259のサポートとnetstandard1.3のサポートを両方含める理由があります。それぞれのTFM (target framework moniker)で示されるフレームワークのサポート対象が異なるからです。(netstandard1.3とPCL Profile 259の関係は包摂的かもしれない。その検証を全てのPCLプロファイルとnetstandardのバージョンについて実施できますか?)

そして、どちらのTFMがより「優先」されるかを判断することはできません。なぜならライブラリ開発者によっては、PCL側により多くの機能を実装しつつnetstandard側は限定的な機能のみを提供しているかもしれないし、逆かもしれないからです(たとえば、bait & switchなPCLあるいはnetstandardなライブラリの有無によって幅広い実装を一方でのみ実現している場合)。

いずれにしろ、netstandard1.3でTFMが解決されると、NuGetは次にそのgroupのdependenciesを取得しにかかります。

  <group targetFramework=".NETStandard1.3">
    <dependency id="NETStandard.Library" version="1.6.1" exclude="Build,Analyzers" />
    <dependency id="Microsoft.CSharp" version="4.3.0" exclude="Build,Analyzers" />
    <dependency id="System.ComponentModel.TypeConverter" version="4.3.0" exclude="Build,Analyzers" />
    <dependency id="System.Runtime.Serialization.Primitives" version="4.3.0" exclude="Build,Analyzers" />
    <dependency id="System.Runtime.Serialization.Formatters" version="4.3.0" exclude="Build,Analyzers" />
    <dependency id="System.Xml.XmlDocument" version="4.3.0" exclude="Build,Analyzers" />
  </group>

これらのパッケージが(a)NETStandard.Libraryの内容から、あるいは(b)Microsoft.CSharpの内容から、実際にダウンロードされてプロジェクトに追加されるのかはわかりませんが、追加された時には netstandard1.3の文脈で追加されているようです。Microsoft.CSharp(4.3.0)について具体的な挙動を見てみると、このパッケージにはMonoAndroidのダミーTFM(実装がないやつ)と、netstandard1.3の実装ありTFMが含まれており、Newtonsoft.Jsonを追加したXamarin.Androidのアプリケーションには(残念ながら)正しいダミー側ではなく間違った実装側が追加されます。

いずれにしろこれらのパッケージが順次解決されていき、.csprojには大量の無関係な参照が追加されていくのです。衝突するふたつのTFMを解決する定性的なやり方は存在しないので、解決策はありません

もっともNewtonsoft.JsonのNuGetパッケージングもいささか怪しいものです。Newtonsoft.Json.nuspecの一部を見てみましょう。

  <group targetFramework=".NETStandard1.3">
    <dependency id="NETStandard.Library" version="1.6.1" exclude="Build,Analyzers" />
    <dependency id="Microsoft.CSharp" version="4.3.0" exclude="Build,Analyzers" />
    ...
  </group>

targetFrameworkは1.3なのにNETStandard.Library 1.6.1を依存関係として引っ張ってきているのっておかしくないですかね? .NET Coreランタイムが古いと動かないような実装かもしれないのに。この辺Newtonsoft.Jsonのやり方は軽率だと思います。とはいえ、だからといってこれを解決しても衝突するふたつのTFMが解決できない問題が解決するわけではありませんし(とわたしは理解しています)、問題は開発者が軽率であることよりも、こういうパッケージングが出来てしまうことではないか、とも考えられます。

ちなみに、 https://docs.microsoft.com/en-us/nuget/reference/target-frameworks からリンクされている Get Nearest FrameworkツールでProject Frameworkにmonoandroid、Package Frameworksにnetstandard1.3 portable-profile259 を渡して診断させると netstandard1.3 が返ってきますが、その判断を支える正当な理由はどこにもありません。

.NET Standard Library 2.0とNuGetまわりでは、「仕様レベルでちゃんと解決できていない問題がある」みたいなmeta issueがあり、とりあえず長すぎて読む気もしないのですが、見切り発車だったなという感想を持たざるを得ない感じです。

そんなわけで現状では「これが仕様上想定される動作だ」ということになるのですが、わたしの予想では、Newtonsoft.Jsonのパッケージングを修正することで問題を解決することができるのではないかと思っています。具体的にはこんな感じです。

<dependencies>
  <group targetFramework="MonoAndroid1.0" />
  ...
</dependencies>

ただhttps://github.com/JamesNK/Newtonsoft.Jsonにはnuspecファイルが無いし、パッケージングはめんどくさいps1で実装しているっぽいんですよね。ちょっと追いかける気がしない。まあ気が向いたらコメントするかもしれません。ここで書いたことをひと通り英語でもう一回書くのは面倒だなあ…