MSBuild Modernization: Legacy to SDK-style Migration
Identifying Legacy vs SDK-style Projects
Legacy indicators:
< Project ToolsVersion = " 15.0 " xmlns = " http://schemas.microsoft.com/developer/msbuild/2003 "
< Import Project = " $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props " /> < PropertyGroup
< Configuration Condition = " ' $(Configuration)' == '' "
Debug </ Configuration
< Platform Condition = " ' $(Platform)' == '' "
AnyCPU </ Platform
< OutputType
Library </ OutputType
< RootNamespace
MyLibrary </ RootNamespace
< AssemblyName
MyLibrary </ AssemblyName
< TargetFrameworkVersion
v4.7.2 </ TargetFrameworkVersion
< FileAlignment
512 </ FileAlignment
< Deterministic
true </ Deterministic
</ PropertyGroup
< Import Project = " $(MSBuildToolsPath)\Microsoft.CSharp.targets " /> </ Project
< Project Sdk = " Microsoft.NET.Sdk "
< PropertyGroup
< TargetFramework
net472 </ TargetFramework
</ PropertyGroup
</ Project
Migration Checklist: Legacy → SDK-style Step 1: Replace Project Root Element BEFORE:
< Project ToolsVersion = " 15.0 " xmlns = " http://schemas.microsoft.com/developer/msbuild/2003 "
< Import Project = " $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props " Condition = " Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props') " />
< Import Project = " $(MSBuildToolsPath)\Microsoft.CSharp.targets " /> </ Project
AFTER: < Project Sdk = " Microsoft.NET.Sdk "
</ Project
Remove the XML declaration, ToolsVersion , xmlns , and both
lines. The Sdk attribute replaces all of them. Step 2: Set TargetFramework BEFORE: < PropertyGroup < TargetFrameworkVersion
v4.7.2 </ TargetFrameworkVersion
</ PropertyGroup
AFTER: < PropertyGroup
< TargetFramework
net472 </ TargetFramework
</ PropertyGroup
TFM mapping table: Legacy TargetFrameworkVersion SDK-style TargetFramework v4.6.1 net461 v4.7.2 net472 v4.8 net48 (migrating to .NET 6) net6.0 (migrating to .NET 8) net8.0 Step 3: Remove Explicit File Includes BEFORE: < ItemGroup
< Compile Include = " Controllers\HomeController.cs " /> < Compile Include = " Models\User.cs " /> < Compile Include = " Models\Order.cs " /> < Compile Include = " Services\AuthService.cs " /> < Compile Include = " Services\OrderService.cs " /> < Compile Include = " Properties\AssemblyInfo.cs " />
</ ItemGroup
< ItemGroup
< Content Include = " Views\Home\Index.cshtml " /> < Content Include = " Views\Shared_Layout.cshtml " />
</ ItemGroup
AFTER: Delete all of these
and item groups entirely. SDK-style projects include them automatically via implicit globbing. Exception: keep explicit entries only for files that need special metadata or reside outside the project directory: < ItemGroup < Content Include = " ..\shared\config.json " Link = " config.json " CopyToOutputDirectory = " PreserveNewest " /> </ ItemGroup
Step 4: Remove AssemblyInfo.cs BEFORE ( Properties\AssemblyInfo.cs ): using System . Reflection ; using System . Runtime . InteropServices ; [ assembly : AssemblyTitle ( "MyLibrary" ) ] [ assembly : AssemblyDescription ( "A useful library" ) ] [ assembly : AssemblyCompany ( "Contoso" ) ] [ assembly : AssemblyProduct ( "MyLibrary" ) ] [ assembly : AssemblyCopyright ( "Copyright © Contoso 2024" ) ] [ assembly : ComVisible ( false ) ] [ assembly : Guid ( "..." ) ] [ assembly : AssemblyVersion ( "1.2.0.0" ) ] [ assembly : AssemblyFileVersion ( "1.2.0.0" ) ] AFTER (in .csproj ): < PropertyGroup
< AssemblyTitle
MyLibrary </ AssemblyTitle
< Description
A useful library </ Description
< Company
Contoso </ Company
< Product
MyLibrary </ Product
< Copyright
Copyright © Contoso 2024 </ Copyright
< Version
1.2.0 </ Version
</ PropertyGroup
Delete Properties\AssemblyInfo.cs — the SDK auto-generates assembly attributes from these properties. Alternative: if you prefer to keep AssemblyInfo.cs , disable auto-generation: < PropertyGroup
< GenerateAssemblyInfo
false </ GenerateAssemblyInfo
</ PropertyGroup
Step 5: Migrate packages.config → PackageReference BEFORE ( packages.config ):
< packages
< package id = " Newtonsoft.Json " version = " 13.0.3 " targetFramework = " net472 " /> < package id = " Serilog " version = " 3.1.1 " targetFramework = " net472 " /> < package id = " Microsoft.Extensions.DependencyInjection " version = " 8.0.0 " targetFramework = " net472 " /> </ packages
AFTER (in .csproj ): < ItemGroup
< PackageReference Include = " Newtonsoft.Json " Version = " 13.0.3 " /> < PackageReference Include = " Serilog " Version = " 3.1.1 " /> < PackageReference Include = " Microsoft.Extensions.DependencyInjection " Version = " 8.0.0 " /> </ ItemGroup
Delete packages.config after migration. Migration options: Visual Studio: right-click packages.config → Migrate packages.config to PackageReference CLI: dotnet migrate-packages-config or manual conversion Binding redirects: SDK-style projects auto-generate binding redirects — remove the
section from app.config if present Step 6: Remove Unnecessary Boilerplate Delete all of the following — the SDK provides sensible defaults:
< Import Project = " $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props " ... /> < Import Project = " $(MSBuildToolsPath)\Microsoft.CSharp.targets " />
< PropertyGroup
< Configuration Condition = " ' $(Configuration)' == '' "
Debug </ Configuration
< Platform Condition = " ' $(Platform)' == '' "
AnyCPU </ Platform
< ProjectGuid
{...} </ ProjectGuid
< OutputType
Library </ OutputType
< AppDesignerFolder
Properties </ AppDesignerFolder
< FileAlignment
512 </ FileAlignment
< AutoGenerateBindingRedirects
true </ AutoGenerateBindingRedirects
< Deterministic
true </ Deterministic
</ 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 = " System " /> < Reference Include = " System.Core " /> < Reference Include = " System.Data " /> < Reference Include = " System.Xml " /> < Reference Include = " System.Xml.Linq " /> < Reference Include = " Microsoft.CSharp " /> </ ItemGroup
< None Include = " packages.config " />
<
Service
Include
=
"
{508349B6-6B84-11D3-8410-00C04F8EF8E0}
"
/>
Keep
only properties that differ from SDK defaults (e.g.,
< TargetFramework
net8.0 </ TargetFramework
< Nullable
enable </ Nullable
< ImplicitUsings
enable </ ImplicitUsings
< LangVersion
latest </ LangVersion
</ PropertyGroup
enable — enables nullable reference type analysisenable — auto-imports common namespaces (.NET 6+)latest — uses the latest C# language version (or specify e.g. 12.0 ) Complete Before/After Example BEFORE (legacy — 65 lines):
< Project ToolsVersion = " 15.0 " xmlns = " http://schemas.microsoft.com/developer/msbuild/2003 "
< Import Project = " $(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props " Condition = " Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props') " /> < PropertyGroup
< Configuration Condition = " ' $(Configuration)' == '' "
Debug </ Configuration
< Platform Condition = " ' $(Platform)' == '' "
AnyCPU </ Platform
< ProjectGuid
{12345678-1234-1234-1234-123456789ABC} </ ProjectGuid
< OutputType
Library </ OutputType
< AppDesignerFolder
Properties </ AppDesignerFolder
< RootNamespace
MyLibrary </ RootNamespace
< AssemblyName
MyLibrary </ AssemblyName
< TargetFrameworkVersion
v4.7.2 </ TargetFrameworkVersion
< FileAlignment
512 </ FileAlignment
< Deterministic
true </ Deterministic
</ 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 = " System " /> < Reference Include = " System.Core " /> < Reference Include = " System.Xml.Linq " /> < Reference Include = " Microsoft.CSharp " /> </ ItemGroup
< ItemGroup
< Compile Include = " Models\User.cs " /> < Compile Include = " Models\Order.cs " /> < Compile Include = " Services\UserService.cs " /> < Compile Include = " Services\OrderService.cs " /> < Compile Include = " Helpers\StringExtensions.cs " /> < Compile Include = " Properties\AssemblyInfo.cs " /> </ ItemGroup
< ItemGroup
< None Include = " packages.config " /> </ ItemGroup
< Import Project = " $(MSBuildToolsPath)\Microsoft.CSharp.targets " /> </ Project
AFTER (SDK-style — 11 lines): < Project Sdk = " Microsoft.NET.Sdk "
< PropertyGroup
< TargetFramework
net472 </ TargetFramework
</ PropertyGroup
< ItemGroup
< PackageReference Include = " Newtonsoft.Json " Version = " 13.0.3 " /> < PackageReference Include = " Serilog " Version = " 3.1.1 " /> </ ItemGroup
</ Project
Common Migration Issues Embedded resources: files not in a standard location may need explicit includes: < ItemGroup
< EmbeddedResource Include = " ..\shared\Schemas*.xsd " LinkBase = " Schemas " /> </ ItemGroup
Content files with CopyToOutputDirectory: these still need explicit entries: < ItemGroup
< Content Include = " appsettings.json " CopyToOutputDirectory = " PreserveNewest " /> < None Include = " scripts*.sql " CopyToOutputDirectory = " PreserveNewest " /> </ ItemGroup
Multi-targeting: change the element name from singular to plural:
< TargetFramework
net8.0 </ TargetFramework
< TargetFrameworks
net472;net8.0 </ TargetFrameworks
WPF/WinForms projects: use the appropriate SDK or properties:
< Project Sdk = " Microsoft.NET.Sdk.WindowsDesktop "
< Project Sdk = " Microsoft.NET.Sdk "
< PropertyGroup
< UseWPF
true </ UseWPF
< UseWindowsForms
true </ UseWindowsForms
</ PropertyGroup
</ Project
Test projects: use the standard SDK with test framework packages: < Project Sdk = " Microsoft.NET.Sdk "
< PropertyGroup
< TargetFramework
net8.0 </ TargetFramework
< IsPackable
false </ IsPackable
</ PropertyGroup
< ItemGroup
< PackageReference Include = " Microsoft.NET.Test.Sdk " Version = " 17.9.0 " /> < PackageReference Include = " xunit " Version = " 2.7.0 " /> < PackageReference Include = " xunit.runner.visualstudio " Version = " 2.5.7 " /> </ ItemGroup
</ Project
Central Package Management Migration Centralizes NuGet version management across a multi-project solution. See https://learn.microsoft.com/en-us/nuget/consume-packages/central-package-management for details. Step 1: Create Directory.Packages.props at the repository root with
true anditems for all packages. Step 2: Remove Version from each project's PackageReference :
< PackageReference Include = " Newtonsoft.Json " Version = " 13.0.3 " />
< PackageReference Include = " Newtonsoft.Json " /> Directory.Build Consolidation Identify properties repeated across multiple .csproj files and move them to shared files. Directory.Build.props (for properties — placed at repo or src root): < Project
< PropertyGroup
< TargetFramework
net8.0 </ TargetFramework
< Nullable
enable </ Nullable
< ImplicitUsings
enable </ ImplicitUsings
< TreatWarningsAsErrors
true </ TreatWarningsAsErrors
< Company
Contoso </ Company
< Copyright
Copyright © Contoso 2024 </ Copyright
</ PropertyGroup
</ Project
Directory.Build.targets (for targets/tasks — placed at repo or src root): < Project
< Target Name = " PrintBuildInfo " AfterTargets = " Build "
< Message Importance = " High " Text = " Built $(AssemblyName) → $(TargetPath) " /> </ Target
</ Project
Keep in individual .csproj files only what is project-specific: < Project Sdk = " Microsoft.NET.Sdk "
< PropertyGroup
< OutputType
Exe </ OutputType
< AssemblyName
MyApp </ AssemblyName
</ PropertyGroup
< ItemGroup
< PackageReference Include = " Serilog " /> < ProjectReference Include = " ..\MyLibrary\MyLibrary.csproj " /> </ ItemGroup
</ Project
Tools and Automation Tool Usage dotnet try-convert Automated legacy-to-SDK conversion. Install: dotnet tool install -g try-convert .NET Upgrade Assistant Full migration including API changes. Install: dotnet tool install -g upgrade-assistant Visual Studio Right-click packages.config → Migrate packages.config to PackageReference Manual migration Often cleanest for simple projects — follow the checklist above Recommended approach: Run try-convert for a first pass Review and clean up the output manually Build and fix any issues Enable modern features (nullable, implicit usings) Consolidate shared settings into Directory.Build.props