MSBuild Modernization: Legacy to SDK-style Migration
Identifying Legacy vs SDK-style Projects
Legacy indicators:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />- Explicit file lists (
<Compile Include="..." />for every.csfile) ToolsVersionattribute on<Project>elementpackages.configfile presentProperties\AssemblyInfo.cswith assembly-level attributes
SDK-style indicators:
<Project Sdk="Microsoft.NET.Sdk">attribute on root element- Minimal content — a simple project may be 10–15 lines
- No explicit file includes (implicit globbing)
<PackageReference>items instead ofpackages.config
Quick check: if a .csproj is more than 50 lines for a simple class library or console app, it is likely legacy format.
Migration Checklist: Legacy → SDK-style
Step 1: Replace Project Root Element
BEFORE:
AFTER:
Remove the XML declaration, ToolsVersion, xmlns, and both <Import> lines. The Sdk attribute replaces all of them.
Step 2: Set TargetFramework
BEFORE:
AFTER:
TFM mapping table:
Step 3: Remove Explicit File Includes
BEFORE:
AFTER:
Delete all of these <Compile> and <Content> 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:
Step 4: Remove AssemblyInfo.cs
BEFORE (Properties\AssemblyInfo.cs):
AFTER (in .csproj):
Delete Properties\AssemblyInfo.cs — the SDK auto-generates assembly attributes from these properties.
Alternative: if you prefer to keep AssemblyInfo.cs, disable auto-generation:
Step 5: Migrate packages.config → PackageReference
BEFORE (packages.config):
AFTER (in .csproj):
Delete packages.config after migration.
Migration options:
- Visual Studio: right-click
packages.config→ Migrate packages.config to PackageReference - CLI:
dotnet migrate-packages-configor manual conversion - Binding redirects: SDK-style projects auto-generate binding redirects — remove the
<runtime>section fromapp.configif present
Step 6: Remove Unnecessary Boilerplate
Delete all of the following — the SDK provides sensible defaults:
Keep only properties that differ from SDK defaults (e.g., <OutputType>Exe</OutputType>, <RootNamespace> if it differs from the assembly name, custom <DefineConstants>).
Step 7: Enable Modern Features
After migration, consider enabling modern C# features:
<Nullable>enable</Nullable>— enables nullable reference type analysis<ImplicitUsings>enable</ImplicitUsings>— auto-imports common namespaces (.NET 6+)- Avoid
<LangVersion>latest— the effective language version is determined by the SDK/compiler defaults, not just the TFM, so builds can silently vary across machines with different SDKs installed. Omit<LangVersion>unless you need to pin a specific version. For reproducible builds, pin the SDK version repo-wide withglobal.json(which indirectly fixes the default language version), or set an explicit numeric<LangVersion>(e.g.<LangVersion>12</LangVersion>) per project to directly control the language version.
Complete Before/After Example
BEFORE (legacy — 65 lines):
AFTER (SDK-style — 11 lines):
Common Migration Issues
Embedded resources: files not in a standard location may need explicit includes:
Content files with CopyToOutputDirectory: these still need explicit entries:
Multi-targeting: change the element name from singular to plural:
WPF/WinForms projects: use the appropriate SDK or properties:
Test projects: use the standard SDK with test framework packages:
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 <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> and <PackageVersion> items for all packages.
Step 2: Remove Version from each project's PackageReference:
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):
Directory.Build.targets (for targets/tasks — placed at repo or src root):
Keep in individual .csproj files only what is project-specific:
Tools and Automation
Recommended approach:
- Run
try-convertfor 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


