Visual Studio 2019 と新 csproj への移行

2019 年 4 月 2 日、Visual Studio 2019 が正式版としてリリース (GA: General Available) されました。この記事では、 Visual Studio 2019 に関連する内容として、C# の新しいプロジェクト形式 (csproj) への移行について記載します。新 csproj 自体は Visual Studio 2017 の頃から利用可能でしたが、WinForms や WPF などのプロジェクトに対する Visual Studio 側の対応も 2019 で進んだようなので、移行するには良いタイミングではないでしょうか。

新 csproj の基本的な構成

新しい csproj 形式では、システム側が既定値を設ける事により、多くの項目が省略可能となりました。これは、Visual Studio による自動生成・更新を前提にした複雑なものから、手動による更新も想定した簡素なものへの転換と言う感じで、特にバージョン管理などの観点から見ると非常に楽になったように思います。

以下に、新 csproj において省略しない方が良い(多くの場合、初期設定とは異なる)ものを記述します。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net45</TargetFramework>
    <LangVersion>latest</LangVersion>
    <Platforms>AnyCPU;x86;x64</Platforms>
    <Optimize>true</Optimize>
    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
    <DefineConstants Condition=" '$(Configuration)' == 'Debug' ">DEBUG;TRACE</DefineConstants>
  </PropertyGroup>
</Project>

TargetFramework には net45, netcoreapp3.0, netstandard2.0 などの文字列を指定します(参考:Target Frameworks Reference for NuGet)。この項目は、旧 csproj の TargetFrameworkVersion から置き換わったものです。また、新 csproj 形式では TargetFrameworks(複数形)を代わりに用いる事で、複数の TargetFramework を ";" (semicolon) で区切って列挙する事もできるようです。

LangVersion には C# のバージョンを指定します(参考:Select the C# language version)。具体的なバージョン番号を指定する事もできますが、多くの場合においては、latest または latestMajor のどちらかだと思います。Visual Studio 2019 においては LangVersion の初期設定は latest なので、この項目は省略しても問題ありません。ただ、AppVeyor 等で継続的インテグレーション (CI: Continuous Integration) を実行する際に意図しない挙動をいくつか確認したため、明記する事にしています。

Platforms は Visual Studio 上で x86 や x64 を選択可能にしたい時に明記します。C#/.NET では多くの場合において AnyCPU で良いと思いますが、Unmanaged なライブラリとの兼ね合いでプラットフォームを固定したい場合などに設定します。

GenerateDocumentationFile は、XML Documentation Comments に従って記述されたコメントに基づいて XML ファイルを生成するかどうかを決定します。尚、XML ファイル自体は後述する DocumentationFile に出力パスを記述すれば、この項目がなくても生成されます。しかし、その場合には生成された XML ファイルが NuGet パッケージ (*.nupkg) に含まれないようです。NuGet パッケージにも含める場合、この項目を true で明記する必要があります。

DefineConstants は必要なシンボルを記述するための項目ですが、省略時には TRACE のみが設定されるようです。多くの場合、Debug モード時には TRACE に加えて DEBUG シンボルの存在も想定されているので、それを明記しています。

既定の Include に関する設定

新 csproj では、ソースファイル等に関する記述をできるだけ省略可能にするために、既定では以下の規則で自動的に Include される事になっています(参考:Default compilation includes in .NET Core projects)。

Element Include Remove
Compile **/*.cs N/A
EmbeddedResource **/*.resx N/A
None **/* **/*.cs; **/*.resx

しかし、これらの Include 規則は邪魔になる事もあるため、無効にする方法もいくつか提供されています。具体的には、上記の全ての規則を無効にするには EnableDefaultItems を、Compile および None の項目のみを無効にするにはそれぞれ EnableDefaultCompileItems, EnableDefaultNoneItems を false で記述します(EnableDefaultEmbeddedResourceItems が存在するかどうかは不明)。

個人的には None の Include 規則には不安に感じる所もあるため、EnableDefaultNoneItems を false に設定した上で自力で明記する事にしています。

NuGet パッケージに関する設定

新 csproj では、従来 AssemblyInfo.cs および *.nuspec に記述されていた内容が全て csproj に統合されました。生成されるアセンブリのメタ情報、および NuGet パッケージに関する項目は下記の通りです。

<PropertyGroup>
  <Version>1.0.0</Version>
  <Authors>clown;cube-soft</Authors>
  <Company>CubeSoft</Company>
  <Description>Some description.</Description>
  <Copyright>Copyright © 2010 CubeSoft, Inc.</Copyright>
  <PackageTags>Ta1;Tag2;Tag3</PackageTags>
  <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
  <PackageProjectUrl>https://github.com/cube-soft/$(AssemblyName)</PackageProjectUrl>
  <PackageIconUrl>https://github.com/cube-soft/$(AssemblyName)/blob/master/Icon.png?raw=true</PackageIconUrl>

  <!-- プロジェクト名と異なる場合 -->
  <Product>Cube.Custom.Product</Product>
  <AssemblyName>Cube.Custom.Product</AssemblyName>
  <AssemblyTitle>Custom Title</AssemblyTitle>
  <RootNamespace>CustomNamespace</RootNamespace>

  <!-- NuGet パッケージの生成可否を明記する場合 -->
  <IsPackable>true</IsPackable>

  <!-- 厳密な名前付けに関する設定 -->
  <SignAssembly>true</SignAssembly>
  <AssemblyOriginatorKeyFile>Cube.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

PackageLicenseExpression には、成果物(NuGet パッケージ)に適用するライセンスを指定します。指定可能な文字列に関しては SPDX License List を参照下さい。これまで、ライセンスは URL で指定していましたが、NuGet の仕様変更により deprecated となったようです。

Product, AssemblyName, AssemblyTitle, RootNamespace の 4 種類は、省略時にはプロジェクト名(csproj 拡張子を除く名前)が設定されます。

IsPackable は、プロジェクトが NuGet パッケージを生成可能かどうかを指定します。既定値は true なので明記する必要性は薄いと思いますが、AppVeyor 等の CI サービスにおいて、自動的に NuGet パッケージを生成して欲しくない時などに false を設定する事があります。

SignAssembly および AssemblyOriginatorKeyFile は、厳密な名前付けに関する項目です。これらの項目は旧 csproj と同じなので、詳細は省略します。

AnyCPU に関する設定

新 csproj は Platform が AnyCPU の場合、初期設定では(例えば)"bin/Release/net45" に出力されます(ちなみに、旧 csproj は "bin/Release")。

<PropertyGroup Condition=" '$(Platform)' == 'AnyCPU' ">
  <OutputPath>bin\Any CPU\$(Configuration)\</OutputPath>
  <DocumentationFile>bin\Any CPU\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup>

多くの場合においては初期設定のままで問題ないと思いますが、AppVeyor 等の CI サービスにおいて意図しない挙動をいくつか確認したため、AnyCPU の OutputPath を上記のように設定しています。尚、新 csproj では OutputPath で設定された文字列に $(TargetFramework) を追加したものが実際の出力パスとなるようです。

Properties.Resources に関する設定

前述した通り、新 csproj において *.resx ファイルは EmbeddedResource として自動的に Include されます。しかし、この状態では Visual Studio 上でのリソース編集が Resources.Designer.cs に反映されません。これを反映させるには、旧 csproj で指定されていた内容を明記する必要があります。

<ItemGroup>
  <Compile Update="Properties\Resources.Designer.cs" DependentUpon="Resources.resx" AutoGen="True" DesignTime="True" />
  <EmbeddedResource Update="Properties\Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
</ItemGroup>

ここで重要なのは、Update 属性を使用している点です。*.resx は全て追加済みとなっているため、Include 属性で明記すると重複エラーとなります。これを回避するには、一つは前述した EnableDefaultItems に false を設定する事、そしてもう一つが Update 属性を使用する事です。

尚、パスの区切り文字を "/" (slash) にした場合、Visual Studio 上でリソースを編集したタイミングで csproj が自動的に編集されてしまいました("Properties/Resources" と "Properties\Resources" が両方とも記述される)。少なくとも .NET Framework をターゲットにしている場合、もうしばらくは "\" (backslash) を使用するほうが無難なようです。

参照に関する設定

新 csproj では、参照に関する記述方法も簡素化されています。特に、これまで packages.config と旧 csproj の 2 種類のファイルに記述されていた NuGet パッケージの参照内容が、新 csproj では PackageReference と言う項目に統合されました。

<ItemGroup>
  <ProjectReference Include="..\Another\Another.csproj" />
  <PackageReference Include="SomePackage" Version="1.0.0" />
  <Reference Include="System.For.Bar" />
</ItemGroup>

ProjectReference や PackageReference は推移的な参照をサポートしており、ProjectReference の ProjectReference や PackageReference の PackageReference などは自動的に解決してくれます。このため、新 csproj には直近のプロジェクトまたは NuGet パッケージの参照のみを追加すれば OK です。ただし、推移的なプロジェクト参照に関しては、全てのプロジェクトが新 csproj 形式でなければならないそうです(参考:新しい csproj 形式)。

削除するファイル一覧

前述したように、新しい csproj 形式では、これまでいくつかのファイルに分割して記述されていた情報が一つのファイルに統合されました。そのため、以下のファイルは必要な情報を csproj に記述した後に削除する必要があります。

  • **/Properties/AssemblyInfo.cs
  • **/packages.config
  • **/*.nuspec

WinForms

ここからは、WinForms および WPF アプリケーションの新 csproj について説明します。

基本的な構成

WinForms の基本的な設定は下記の通りです。尚、これまで記述した内容の多くを省略していますが、それらの設定も必要に応じて追加して下さい。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net45</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
    <ApplicationIcon>App.ico</ApplicationIcon>
    <ApplicationManifest>App.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

重要な点は、Project の Sdk 属性を Microsoft.NET.Sdk.WindowsDesktop にする事と、UseWindowsForms を true で明記する事です(参考:Windows Desktop)。これらの項目の意味については、後述します。

OutputType の規定値は Library のようです。そのため、WinForms や WPF アプリケーションの場合、この項目を WinExe に設定します。

ApplicationIcon および ApplicationManifest には、アイコン等のファイルを指定します。これらの項目は旧 csproj と同じなので、詳細は省略します。

Microsoft.NET.Sdk.WindowsDesktop に関して

Visual Studio 2019 かつ .NET Core 3.0 の環境において新たに指定可能となった Microsoft.NET.Sdk.WindowsDesktop および UseWindowsForms ですが、手元の環境で確認した限り、.NET Framework では既定の参照内容に影響を及ぼすようです*1。以下の図は、UseWindowsForms の設定値と Visual Studio 2019 上で確認できる参照内容の対応関係を表したものになります。

UseWindowsForms の設定による違い

この図を見ると、UseWindowsForms が true の場合、WinForms ライブラリである System.Windows.Forms が参照に追加されている事が分かります。他に UseWPF と言う項目も存在しますが、true にした場合には同様に PresentationCore 等の WPF に必要なライブラリが参照に追加されます。ただし、これらの項目は Sdk が Microsoft.NET.Sdk.WindowsDesktop の場合のみ有効で、Microsoft.NET.Sdk の場合は無視されます。

注意点として、Visual Studio 2017 では、.NET Core 3.0 SDK の有効・無効に関わらず Microsoft.NET.Sdk.WindowsDesktop を指定したプロジェクトの読み込みに失敗します。また、Visual Studio 2019 であっても .NET Core 3.0 が無効の場合、ビルドに失敗します。したがって、これらの環境との互換性を考慮する場合、Sdk に Microsoft.NET.Sdk を指定した上で各種ライブラリの参照を明記する必要があります。

Include および Update 規則

WinForms におけるソースファイル等の Include および Update 規則に関する記述内容は下記になります。

<ItemGroup>
  <Compile Update="Views\**\*.cs" SubType="Form" />
  <Compile Update="Views\**\*.Designer.cs" SubType="Code" />
  <EmbeddedResource Update="Views\**\*.resx" DependentUpon="%(Filename).cs" />
  <None Include="Assets\**\*" />
  <None Include="App.config" />
</ItemGroup>

この中で最低限必要な項目は EmbeddedResource です。前述したように *.resx は自動的に Include されますが、これだけだと Form にアイコンを設定している場合などで実行時エラーが発生します。これを防止するために WinForms に関連する *.resx に対して、DependentUpon 属性を指定する形で Update します。尚、Include や Update にワイルドカードで指定した場合、"%(Filename)" で各ファイルの拡張子を除いた名前を取得できるようです。

2 種類の Compile 設定は、各種 Form を Visual Studio のデザイナ上で編集可能にするための設定です。ただし、これらの設定は Visual Studio 2017 では効果がありませんでした。

WPF

次は、WPF アプリケーションの新 csproj について説明します。

基本的な構成

WPF の基本的な設定は下記の通りです。UseWindowsForms が UseWPF に置き換わったのみで、それ以外は全て WinForms と同様です。

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net45</TargetFramework>
    <LangVersion>latest</LangVersion>
    <UseWPF>true</UseWPF>
    <EnableDefaultNoneItems>false</EnableDefaultNoneItems>
    <ApplicationIcon>App.ico</ApplicationIcon>
    <ApplicationManifest>App.manifest</ApplicationManifest>
  </PropertyGroup>
</Project>

尚、前述したように UseWindowsForms および UseWPF は各ライブラリの参照設定に関するもののようなので、併記する事も可能です。WPF の場合、GUI コンポーネントが足りない等の理由で WinForms を併用する事もあるので、その場合には UseWindowsForms も true で設定します。

Include および Update 規則

WPF におけるソースファイル等の Include および Update 規則に関する記述内容は下記になります。

<ItemGroup>
  <ApplicationDefinition Include="App.xaml" SubType="Designer" Generator="MSBuild:Compile" />
  <Page Include="Views\**\*.xaml" SubType="Designer" Generator="MSBuild:Compile" />
  <Compile Update="Views\**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />
  <Resource Include="Assets\**\*" />
  <Resource Include="App.ico" />
  <None Include="App.config" />
</ItemGroup>

まず、App.xaml を ApplicationDefinition で明記する必要があります。次に、Page および Compile の項目で、それ以外の *.xaml ファイル(およびコードビハインドに当たる *.cs ファイル)を追加します。この時、App.xaml が同じディレクトリに存在すると重複エラーとなります。その場合は、Page の項目に対して Exclude="App.xaml" と言う記述を追加して下さい。個人的には、ディレクトリ階層で区分する事で解決するようにしています。

新 csproj 具体例へのリンク一覧

最後に、これまでに説明した内容を実際のプロジェクトで記述したものをサンプルとしていくつか紹介します。

*1:尚、TargetFramework を netcoreapp3.0 に設定した場合、現時点では Microsoft.NET.Sdk.WindowsDesktop と記述した時点で UseWindowsForms, UseWPF の設定に関わらず全ての WinForms/WPF ライブラリが参照に追加されるようです。