将 WPF、UWP 以及其他各种类型的旧样式的 csproj 文件迁移成新样式的 csproj 文件

写过 .NET Standard 类库或者 .NET Core 程序的你一定非常喜欢微软为他们新开发的项目文件(对于 C#,则是 csproj 文件)。这种文件非常简洁,组织一个庞大的项目也只需要聊聊二三十行;也非常易读,你可以轻易地修改其代码而不用经过过多的提前学习。当然,微软曾经尝试过用 project.json 来组织项目文件,不过只有短短的预览版阶段用过,此后就废弃了。

然而组织传统 .NET Framework 类库的 csproj 文件却极其庞大且难以理解。而本文将提供一种迁移方法,帮助你完成这样的迁移,以便体验新 csproj 文件带来的诸多好处。


新 csproj 文件的优势与直观体验

如果你已经体验过新 csproj 文件的好处,那么直接前往下一节即可。没体验过的话就来体验一下吧!

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net471</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
    <PackageReference Include="MSTest.TestFramework" Version="1.2.0" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\Walterlv.Demo.csproj" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="System.ComponentModel.Composition" />
  </ItemGroup>
</Project>

这是我的一个单元测试项目的 csproj 文件,是不是非常简洁?基于 .NET Framework 4.7.1,引用 MSTest v2,测试 Walterlv.Demo 项目,引用了一个 .NET Framework 类库。

其依赖的显示也非常简洁:

简洁的依赖

而传统的 csproj 文件是怎样的呢?

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.props" Condition="Exists('..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProjectGuid>{F0E83A94-D65F-492D-AF5B-CC43666FE676}</ProjectGuid>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <RootNamespace>Walterlv.UnitTests.Demo</RootNamespace>
    <AssemblyName>Walterlv.UnitTests.Demo</AssemblyName>
    <TargetFrameworkVersion>v4.7.1</TargetFrameworkVersion>
    <FileAlignment>512</FileAlignment>
    <ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
    <ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
    <IsCodedUITest>False</IsCodedUITest>
    <TestProjectType>UnitTest</TestProjectType>
    <NuGetPackageImportStamp>
    </NuGetPackageImportStamp>
    <TargetFrameworkProfile />
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
      <HintPath>..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions, Version=14.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
      <HintPath>..\packages\MSTest.TestFramework.1.2.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll</HintPath>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.ComponentModel.Composition" />
    <Reference Include="System.Core" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="DemoTest.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
  </ItemGroup>
  <ItemGroup>
    <None Include="packages.config" />
  </ItemGroup>
  <Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />
  <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
  <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
    <PropertyGroup>
      <ErrorText>这台计算机上缺少此项目引用的 NuGet 程序包。使用“NuGet 程序包还原”可下载这些程序包。有关更多信息,请参见 http://go.microsoft.com/fwlink/?LinkID=322105。缺少的文件是 {0}。</ErrorText>
    </PropertyGroup>
    <Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.props'))" />
    <Error Condition="!Exists('..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.targets'))" />
  </Target>
  <Import Project="..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.targets" Condition="Exists('..\packages\MSTest.TestAdapter.1.2.0\build\net45\MSTest.TestAdapter.targets')" />
</Project>

而且,还要搭配一个 packages.config 文件来描述 NuGet。

从对比中我们就能明显看出新 csproj 文件的优势:

  1. 文件小,易读易写
  2. 在版本管理中更容易解冲突
  3. NuGet 包的引用没有路径要求,这意味着开发者可以任意指定 NuGet 包的位置
  4. 嵌套的引用不需要重复指定(如果 A 引用了 B,B 引用了 C;那么 A 不需要显式引用 C 也能调用到 C)
  5. 可以一遍编辑 csproj 一边打开项目,互不影响
  6. 可以指定多个开发框架,详见 让一个项目指定多个开发框架 - 吕毅的博客

迁移普通 .NET Framework 类库的项目文件

目前只有基于 .NET Core 和 .NET Standard 的普通项目能够使用这种新的 csproj 文件。在 GitHub 的讨论(XAML files are not supported · Issue #1467 · dotnet/project-system)中,.NET Core 的开发者们是这么说的。

不过,.NET Framework 项目也能够有限地得到支持。具体可支持的类型以及迁移方法我的小伙伴写了一篇博客,请前往此处查看:从以前的项目格式迁移到 VS2017 新项目格式 - 林德熙

目前没有自动的迁移方法,至少在我的实际迁移过程中,只有少数项目能够直接编译通过。由于以上我的小伙伴给出了具体的迁移方法,所以此处我只给出迁移思路。

手动迁移

第一步:将以下代码复制到原有的 csproj 文件中(不管原来的文件里有多少内容)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net47</TargetFramework>
  </PropertyGroup>
</Project>

第二步:修改目标 .NET Framework 框架版本号,比如 net45、net462、net471。

第三步:安装此前已经安装好的 NuGet 包,或者把原来的 packages.config 文件里的 NuGet 配置复制到 csproj 文件中,并统一修改格式:

<package id="MSTest.TestAdapter" version="1.2.0" targetFramework="net45" />
<package id="MSTest.TestFramework" version="1.2.0" targetFramework="net45" />

修改成

<PackageReference Include="MSTest.TestAdapter" Version="1.2.0" />
<PackageReference Include="MSTest.TestFramework" Version="1.2.0" />

第四步:引用此前引用过的类库文件和项目引用

第五步:删除 Properties 文件夹和里面的所有文件,因为这些信息已经被 csproj 文件记录并自动生成了。

手动迁移过程中可能遇到的坑

如果你的项目比较小,比较新,比较少折腾,那么走完上面的五个步骤基本上你应该能够直接编译通过并运行了。不过,能做到这些的项目其实真不多,基本上或多或少都会遇到一些坑。

比如,你可能曾经排除出项目之外的文件现在又回来了——现在,你需要重新将他们排除,或者直接删除掉!

比如,你可能放入项目的不止有 cs 文件,还有其他各种用途的资源——你需要重新选中他们然后在属性面板中设置文件的生成属性。

比如,你可能有一些 xaml 文件——这时,你需要看本文的下一个章节 迁移 WPF/UWP 这类 XAML UI 类库的项目文件

自动迁移

在多次的迁移中,发现单元测试项目通常是最好迁移的。如果没有遇到什么坑,那么之前建议的五个步骤其实可以自动化完成。这里我并没有找到这样的迁移工具,但应该有大神已经做出来了。

迁移 WPF/UWP 这类 XAML UI 类库的项目文件

UWP 项目已经是 .NET Core 了,然而它依然还在采用旧样式的 csproj 文件,一个让人感到失望的原因竟是——不支持 XAML!这简直不可思议,身为微软大力推广的项目类型和文件类型竟然在新格式里面属于不支持的类型!

不过,毕竟是微软,虽没有原生支持,可新格式也依旧有些扩展性,可以变相的解决这一问题。

WPF/UWP 项目迁移过程中的困难点

对于 WPF 类库,使用以上的方法迁移之后,编译几乎必定是报错的:

WPF 项目迁移后编译不通过
▲ 此图中的 CSharpArgument 错误是因为缺少 Microsoft.CSharp 引用,添加即可解决。

找不到 InitializeComponent 则几乎可以认定为 XAML 文件没有被编译成 .g.cs 文件,或者简单点儿说是 XAML 没有被有效识别。GitHub 上 dotnet 的几个项目都有与 XAML 支持相关的讨论,比如 XAML files are not supported · Issue #1467 · dotnet/project-systemXAML files are not supported · Issue #810 · dotnet/sdk

庆幸的是,Stack Overflow 上 stil 较完整地整理了 XAML 支持的 csproj 文件改造方法。来自于 c# - How-to migrate Wpf projects to the new VS2017 format - Stack Overflow

<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
  <PropertyGroup>
    <LanguageTargets>$(MSBuildExtensionsPath)\$(VisualStudioVersion)> \Bin\Microsoft.CSharp.targets</LanguageTargets>
    <TargetFramework>net47</TargetFramework>
    <OutputType>Exe</OutputType>> 
    <StartupObject />
  </PropertyGroup>

  <ItemGroup>
    <!-- App.xaml -->
    <ApplicationDefinition Include="App.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
    </ApplicationDefinition>

    <!-- XAML elements -->
    <Page Include="**\*.xaml" Exclude="App.xaml">
      <SubType>Designer</SubType>
      <Generator>MSBuild:UpdateDesignTimeXaml</Generator>
    </Page>
    <Compile Update="**\*.xaml.cs" SubType="Code" DependentUpon="%(Filename)" />

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

    <!-- Settings -->
    <None Update="Properties\Settings.settings" Generator="SettingsSingleFileGenerator" LastGenOutput="Settings.Designer.cs" />
    <Compile Update="Properties\Settings.Designer.cs" AutoGen="True" DependentUpon="Settings.settings" />

  </ItemGroup>

  <ItemGroup>
    <Reference Include="PresentationCore" />
    <Reference Include="PresentationFramework" />
    <Reference Include="System.Xaml" />
    <Reference Include="WindowsBase" />
  </ItemGroup>
</Project>

需要注意,<OutputType /><StartupObject /><ApplicationDefinition /> 如果是类库则需要去掉。

这样,再次编译后,错误窗口中则没有错误了。

没有错误的错误窗口

等等!!!然而还是编译不通过!在输出窗口中我们可以看到另外的错误:

输出窗口中的错误

困难点 细节
默认不支持 XAML XAML 文件在编译中的默认行为是 None
不能天然编译 XAML 即便引入了 XAML 文件,编译依然失败
XAML 不支持智能感知提示  
XAML 文件在项目中不可见 设置了 Page 编译的 XAML 文件在项目中不可见

未完待续……

迁移中各种诡异的报错及其解决方法

对于带 XAML 的项目,如果在迁移过程中放弃了,试图恢复成原来的方案,那么在编译时会发生一个诡异的错误:

错误

错误

就是试图迁移的那个项目!无论依赖了谁还是被谁依赖,都是此项目发生“NuGet”错误。而要解决此问题,需要几乎重置 git 仓库,然后重新还原 NuGet 包。

迁移之后的劣势

迁移成新的 csproj 格式之后,新格式中不支持的配置会丢失。


参考资料

本文会经常更新,请阅读原文: https://walterlv.github.io/post/introduce-new-style-csproj-into-net-framework.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名 吕毅 (包含链接: https://walterlv.github.io ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系