Stop using the old *.csproj SDK format

Migrate your legacy csproj files to the new SDK format in 8 steps

Ikechi Michael
3 min readApr 22, 2024

My last article, “Using glob patterns in *.csproj files” helps make your legacy *.csproj files leaner, cleaner and more reliable, but let’s take it even further.

Why be stuck with the legacy *.csproj file format? You see, if your *.csproj file looks like this:

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

That is the old csproj format, and there is just no good reason to continue using it.

Why?

  1. You have to reference all *.cs files you intend to publish, and while you can use glob patterns (and you should, if you’re stuck with the old csproj format)

The drawbacks of the old *.csproj format

  1. Manual File References: In the old *.csproj format, you have to reference all *.cs files you intend to publish as well as each file you want to copy to the output directory. This means listing each file individually, leading to a cluttered and error-prone project file.
  2. Package Management Sucks: You have to reference DLLs directly. If the location changes, the error messages are not helpful. Granted, Visual Studio handles a lot of this for you, but if you haven’t used the new csproj format, you won’t even know what you’re missing.
  3. Overly Verbose: With workflows like Git where you have to review changes to the *.csproj files, having a compact file with information that is mostly useful to you at a glance, helps.
  4. Can’t use the Dotnet CLI: Imagine you could make a change, and instantly have access to dotnet build , dotnet run , dotnet publish , instead of relying on Visual Studio to use msbuild in the background for everything because the syntax for msbuild is too verbose and you never bothered to learn it.

Making the Transition

Let’s migrate to the new *.csproj file.

1. Create a new *.csproj file

First, we begin by creating your new *.csproj file:

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo><!--important for ASP.NET projects -->
<LangVersion>preview</LangVersion>
<Version>1.0.0</Version>
</PropertyGroup>
<PropertyGroup>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
</PropertyGroup>
</Project>

2. Include References

Do you have a bunch of <Reference Include="..." /> in your old *.csproj file? Add them:

<ItemGroup>
<Reference Include="System" />
<Reference Include="..." />
</ItemGroup>

Do you have <Reference /> statements that point to DLL files, such as:

<ItemGroup>
<Reference Include="Azure.Core, Version=1.35.0.0, Culture=neutral, PublicKeyToken=92742159e12e44c8, processorArchitecture=MSIL">
<HintPath>..\..\..\packages\Azure.Core.1.35.0\lib\net472\Azure.Core.dll</HintPath>
</Reference>
</ItemGroup>

Let’s add them, but let’s use Nuget packages instead where we can:

<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.35.0" />
</ItemGroup>

3. Ignore Compile statements

You’ll likely have statements like:

<ItemGroup>
<Compile Include="App_State/Foo.cs" />
</ItemGroup>

Ignore them!

4. Move Content statements

When you see statements like:

<ItemGroup>
<Content Include="packages.config">
<SubType>Designer</SubType>
</Content>
<Content Include="Global.asax" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Areas\HelpPage\Views\Web.config" />
<Content Include="Areas\HelpPage\Views\Shared\_Layout.cshtml" />
<Content Include="Areas\HelpPage\Views\Help\ResourceModel.cshtml" />
</ItemGroup>

Add them, using glob patterns where you can:

<ItemGroup>
<Content Include="packages.config">
<SubType>Designer</SubType>
</Content>
<Content Include="Global.asax" CopyToOutputDirectory="PreserveNewest" />
<Content Include="Areas\**\Web.config" />
<Content Include="Areas\**\*.cshtml" />
</ItemGroup>

5. Move your Project References

When you see statements like:

<ItemGroup>
<ProjectReference Include="..\Foo\Foo.csproj">
<Project>{bf10a583-4d81-4794-a9cc-8083a296a8ad}</Project>
<Name>Foo</Name>
</ProjectReference>
</ItemGroup>

Rejoice! Because they get even simpler when we move them. Let’s move them:

<ItemGroup>
<ProjectReference Include="..\Foo\Foo.csproj" />
</ItemGroup>

6. Move all Target statements

All statements looking like:

<Target Name="...">
...
</Target>

should be migrated.

7. Ignore Import statements

I haven’t missed any of the <Import ... /> statements that were in my legacy *.csproj in my new project files since migrating, so I’ll say feel free to ignore statements like:

<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />

8. Now, for ASP.NET projects

If migrating an ASP.NET project, I’ve found it very useful to add the following:

<PropertyGroup>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath Condition="$(UseWPP_CopyWebApplication) != 'true'">bin\</OutputPath>
<OutputPath Condition="$(UseWPP_CopyWebApplication) == 'true'">bin\bin\</OutputPath>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="ConfigTransformationTool" Version="1.5.1" />
</ItemGroup>
<Target Name="CopyWebsiteFilesOnPublish" AfterTargets="Build" Condition="$(UseWPP_CopyWebApplication) == 'true'">
<Exec Command="powershell.exe -WindowStyle Hidden -NonInteractive -Command &quot;Move-Item $(OutDir)Areas $(OutDir)../Areas&quot; -Force" IgnoreExitCode="true" />
<Exec Command="powershell.exe -WindowStyle Hidden -NonInteractive -Command &quot;Move-Item $(OutDir)App_Data $(OutDir)../App_Data&quot; -Force" IgnoreExitCode="true" />
<Exec Command="powershell.exe -WindowStyle Hidden -NonInteractive -Command &quot;Move-Item $(OutDir)Global.asax $(OutDir)../Global.asax&quot; -Force" IgnoreExitCode="true" />
<Exec Command="$(NuGetPackageRoot)ConfigTransformationTool\1.5.1\tools\ctt.exe source:Web.config transform:Web.$(Configuration).config destination:$(OutDir)../Web.config" />
</Target>

This ensures that the output is built to the bin directory, and if using the -p:UseWPP_CopyWebApplication=true compiler option, then it goes to bin/bin , so that your Web.config and Global.asax can live in the bin .

Now, go ahead and build!

Note: If your project references other projects that use the old csproj format, they will also have to be migrated using the above steps.

Did it work? Let me know in the comments.

--

--

Ikechi Michael

I’ve learned I don’t know anything. I've also learned that people will pay for what I know. Maybe that's why they never pay.