You may have heard the buzz: .NET Core went from the project.json to csproj file format, and the new csproj format is leaner, easier to read, and adds new features. But what about your .NET Framework VS 2015 (or 2013) project? How can you participate in the VS 2017 goodness? Keep reading: I’ll show you some of the major changes, and how to upgrade to VS 2017.

Disclaimer: this only works for a small set of project types.

  • class library projects
  • console apps
  • ASP.NET Core web apps
  • .NET Core

If you are building WPF, Universal Windows, Xamarin, or ASP.NET 4 projects, you’ll have to stick with the old format (for now… follow https://github.com/dotnet/sdk/issues/491 for updates).

Approach one: start over

Delete everything and start over. Seriously consider this. It might be the easiest way to upgrade. (Make a backup copy first, obviously.)

Replace the contents of your csproj with the following, based on your project type:

Class library

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

Console app

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net46</TargetFramework>
  </PropertyGroup>
</Project>

Test project

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net46</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
    <PackageReference Include="xunit" Version="2.2.0" />
    <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
  </ItemGroup>
</Project>

Approach two: manually convert

Trim the fat manually.

Unfortunately, there is no magic button or command line tool to upgrade from VS 2015 to VS 2017 formats.

:warning: Keep your code in source control.

First line

Old

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

New

<Project Sdk="Microsoft.NET.Sdk">

Must delete

These lines must be removed to make the new format work:

<!-- usually at the top of the file -->
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />

<!-- usually at the bottom -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets" Condition="Exists('$(VSToolsPath)\TeamTest\Microsoft.TestTools.targets')" />

Test projects usually have one or many block snamed <Choose>. These must be deleted too:

<Choose>
    <When Condition="('$(VisualStudioVersion)' == '10.0' or '$(VisualStudioVersion)' == '') and '$(TargetFrameworkVersion)' == 'v3.5'">
      <ItemGroup>
        <Reference Include="Microsoft.VisualStudio.QualityTools.UnitTestFramework, Version=10.1.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL" />
      </ItemGroup>
    </When>
    <Otherwise />
  </Choose>

Must change

These lines must be changed:

Old:

<PropertyGroup>
  <!-- ... -->
  <TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
</PropertyGroup>

New:

<PropertyGroup>
  <!-- v4.5.2 => net452. v4.6 => net46, etc.-->
  <TargetFramework>net452</TargetFramework>
</PropertyGroup>

That massive list of files

You can can delete any lines nested in an <ItemGroup> that match with <Compile> or <EmbeddedResource>. This gets special mention, however, because their are excpetions.

By default, the new SDK will pick up default globbing patterns.

<!-- the defaults -->
<Compile Include="**\*.cs" />
<EmbeddedResource Include="**\*.resx" />

If you have anything outside of the project folder, you’ll need to keep those lines so VS and the compiler know where to find your code.

What to keep

These are common, non-default settings:

<PropertyGroup>
  <Optimize>true</Optimize>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <OutputType>Exe</OutputType>
<!-- when RootNamespace != the csproj file name -->
<RootNamespace />ClassLibrary1</RootNamespace>
<!-- when AssemblyName != the csproj file name -->
<AssemblyName>ClassLibrary1</AssemblyName>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  <DefineConstants>$(DefineConstants);MY_CUSTOM_CONSTANT</DefineConstants>
  <Platform>x64 or x86</Platform>
</PropertyGroup>

<ItemGroup>
  <!-- keep references unless they are to package files. See the section about the NuGet upgrade below. -->
  <Reference Include="System.Configuration" />
</ItemGroup>

Can remove

:warning: Heads up: these are suggestions for things you don’t need in most cases, but not all.

Project references

Old

<ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj">
  <Project>{2C7DF870-5B35-49EF-963D-EE1E72C3177E}</Project>
  <Name>ClassLibrary1</Name>
</ProjectReference>

New

<ProjectReference Include="..\ClassLibrary1\ClassLibrary1.csproj" />

Also, MSBuild now does transitive project-to-project references automatically.

Example: Project A => Project B => Project C.

Old

Both B and C had to be listed.

<!-- in projectA.csproj -->
<ProjectReference Include="..\ProjectB\ProjectB.csproj">
  <Project>{A900C843-8340-421B-B4F0-6C65A0D093C4}</Project>
  <Name>ProjectB</Name>
</ProjectReference>
<ProjectReference Include="..\ProjectC\ProjectC.csproj">
  <Project>{871AC142-FC46-49F5-A5E0-90436648B9C5}</Project>
  <Name>ProjectB</Name>
</ProjectReference>

New

Only B needs to be listed. Project C will automatically be referenced as long as Project B references it.

<!-- in projectA.csproj -->
<ProjectReference Include="..\ProjectB\ProjectB.csproj" />

PropertyGroup

Bring out the red ink

Now the fun part, start deleting tons of stuff. The following elements under <PropertyGroup> can be deleted.

<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace />ClassLibrary1</RootNamespace>
<AssemblyName>ClassLibrary1</AssemblyName>
<Optimize>false</Optimize>
<ProjectGuid>xyz</ProjectGuid>
<ProjectTypeGuids>{3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<FileAlignment>512</FileAlignment>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NuGetPackageImportStamp></NuGetPackageImportStamp>
<TargetFrameworkProfile />
<TestProjectType>UnitTest</TestProjectType>
<IsCodedUITest>False</IsCodedUITest>
<ReferencePath>$(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages</ReferencePath>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>

You likely also have duplicates of these properties under a PropertyGroup that looks like this:

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

That whole block can usually be deleted too.

NuGet: imports and references

This gets special mention.

Old

In NuGet 2, you had a packages.config file AND you had <Import> and <Reference> items in the csproj file.

<Import Project="..\..\packages\xunit.runner.visualstudio.2.2.0\build\net20\xunit.runner.visualstudio.props" Condition="Exists('..\..\packages\xunit.runner.visualstudio.2.2.0\build\net20\xunit.runner.visualstudio.props')" />

<ItemGroup>
  <None Include="packages.config" />

  <Reference Include="MySql.Data, Version=6.9.9.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d, processorArchitecture=MSIL">
  <HintPath>..\..\packages\MySql.Data.6.9.9\lib\net45\MySql.Data.dll</HintPath>
  <Private>True</Private>
</Reference>
</ItemGroup>

New

Delete the packages.config file. Delete any <Reference> and <Import> items that refer to files from packages.

Replace them with <PackageReference>.

<ItemGroup>
  <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
  <PackageReference Include="MySql.Data" Version="6.9.9" />
</ItemGroup>

About transitive references

In NuGet 2, you listed every single package to be included. In NuGet 4 (VS 2017), you only need to list top-level dependencies. Everything else is that those top-level dependencies require will be imported automatically.

Old

<?xml version="1.0" encoding="utf-8"?>
<!-- in packages.config -->
<packages>
  <package id="xunit" version="2.2.0" targetFramework="net452" />
  <package id="xunit.abstractions" version="2.0.1" targetFramework="net452" />
  <package id="xunit.assert" version="2.2.0" targetFramework="net452" />
  <package id="xunit.core" version="2.2.0" targetFramework="net452" />
  <package id="xunit.extensibility.core" version="2.2.0" targetFramework="net452" />
  <package id="xunit.extensibility.execution" version="2.2.0" targetFramework="net452" />
  <package id="xunit.runner.visualstudio" version="2.2.0" targetFramework="net452" developmentDependency="true" />
</packages>

:warning: Figuring this out will be trial and error. Best wishes if you have a long list.

New

<!-- in csproj -->
<ItemGroup>
  <PackageReference Include="xunit" Version="2.2.0" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
  <!-- Notice that xunit.assert, xunit.core, etc. are imported automatically.-->
</ItemGroup>

Summary

Did I miss something? Not sure if something can be removed? Comment below and I’ll try to help out.