Skills msbuild-modernization
Guide for modernizing and migrating MSBuild project files to SDK-style format. Only activate in MSBuild/.NET build context. USE FOR: converting legacy .csproj/.vbproj with verbose XML to SDK-style, migrating packages.config to PackageReference, removing Properties/AssemblyInfo.cs in favor of auto-generation, eliminating explicit <Compile Include> lists via implicit globbing, consolidating shared settings into Directory.Build.props. Indicators of legacy projects: ToolsVersion attribute, <Import Project=\"$(MSBuildToolsPath)\">, .csproj files > 50 lines for simple projects. DO NOT USE FOR: projects already in SDK-style format, non-.NET build systems (npm, Maven, CMake), .NET Framework projects that cannot move to SDK-style. INVOKES: dotnet try-convert, upgrade-assistant tools.
git clone https://github.com/dotnet/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dotnet/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/dotnet-msbuild/skills/msbuild-modernization" ~/.claude/skills/dotnet-skills-msbuild-modernization && rm -rf "$T"
plugins/dotnet-msbuild/skills/msbuild-modernization/SKILL.mdMSBuild Modernization: Legacy to SDK-style Migration
Identifying Legacy vs SDK-style Projects
Legacy indicators:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />- Explicit file lists (
for every<Compile Include="..." />
file).cs
attribute onToolsVersion
element<Project>
file presentpackages.config
with assembly-level attributesProperties\AssemblyInfo.cs
SDK-style indicators:
attribute on root element<Project Sdk="Microsoft.NET.Sdk">- Minimal content — a simple project may be 10–15 lines
- No explicit file includes (implicit globbing)
items instead of<PackageReference>packages.config
Quick check: if a
.csproj is more than 50 lines for a simple class library or console app, it is likely legacy format.
<!-- Legacy: ~80+ lines for a simple library --> <?xml version="1.0" encoding="utf-8"?> <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> <!-- ... 60+ more lines ... --> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> </Project>
<!-- SDK-style: ~8 lines for the same library --> <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net472</TargetFramework> </PropertyGroup> </Project>
Migration Checklist: Legacy → SDK-style
Step 1: Replace Project Root Element
BEFORE:
<?xml version="1.0" encoding="utf-8"?> <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')" /> <!-- ... project content ... --> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> </Project>
AFTER:
<Project Sdk="Microsoft.NET.Sdk"> <!-- ... project content ... --> </Project>
Remove the XML declaration,
ToolsVersion, xmlns, and both <Import> 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 | SDK-style |
|---|---|
| |
| |
| |
| (migrating to .NET 6) | |
| (migrating to .NET 8) | |
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" /> <!-- ... 50+ more lines ... --> </ItemGroup> <ItemGroup> <Content Include="Views\Home\Index.cshtml" /> <Content Include="Views\Shared\_Layout.cshtml" /> <!-- ... more content files ... --> </ItemGroup>
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:
<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):
<?xml version="1.0" encoding="utf-8"?> <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
→ Migrate packages.config to PackageReferencepackages.config - CLI:
or manual conversiondotnet migrate-packages-config - Binding redirects: SDK-style projects auto-generate binding redirects — remove the
section from<runtime>
if presentapp.config
Step 6: Remove Unnecessary Boilerplate
Delete all of the following — the SDK provides sensible defaults:
<!-- DELETE: SDK imports (replaced by Sdk attribute) --> <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" ... /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <!-- DELETE: default Configuration/Platform (SDK provides these) --> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{...}</ProjectGuid> <OutputType>Library</OutputType> <!-- keep only if not Library --> <AppDesignerFolder>Properties</AppDesignerFolder> <FileAlignment>512</FileAlignment> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <Deterministic>true</Deterministic> </PropertyGroup> <!-- DELETE: standard Debug/Release configurations (SDK defaults match) --> <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> <!-- DELETE: framework assembly references (implicit in SDK) --> <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> <!-- DELETE: packages.config reference --> <None Include="packages.config" /> <!-- DELETE: designer service entries --> <Service Include="{508349B6-6B84-11D3-8410-00C04F8EF8E0}" />
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:
<PropertyGroup> <TargetFramework>net8.0</TargetFramework> <Nullable>enable</Nullable> <ImplicitUsings>enable</ImplicitUsings> <LangVersion>latest</LangVersion> </PropertyGroup>
— enables nullable reference type analysis<Nullable>enable</Nullable>
— auto-imports common namespaces (.NET 6+)<ImplicitUsings>enable</ImplicitUsings>
— uses the latest C# language version (or specify e.g.<LangVersion>latest</LangVersion>
)12.0
Complete Before/After Example
BEFORE (legacy — 65 lines):
<?xml version="1.0" encoding="utf-8"?> <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:
<!-- Single target --> <TargetFramework>net8.0</TargetFramework> <!-- Multiple targets --> <TargetFrameworks>net472;net8.0</TargetFrameworks>
WPF/WinForms projects: use the appropriate SDK or properties:
<!-- Option A: WindowsDesktop SDK --> <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <!-- Option B: properties in standard SDK (preferred for .NET 5+) --> <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <UseWPF>true</UseWPF> <!-- or --> <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 <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> and <PackageVersion> items for all packages.
Step 2: Remove
Version from each project's PackageReference:
<!-- BEFORE --> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <!-- AFTER --> <PackageReference Include="Newtonsoft.Json" />
Directory.Build Consolidation
Identify properties repeated across multiple
.csproj files and move them to shared files.
(for properties — placed at repo or src root):Directory.Build.props
<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>
(for targets/tasks — placed at repo or src root):Directory.Build.targets
<Project> <Target Name="PrintBuildInfo" AfterTargets="Build"> <Message Importance="High" Text="Built $(AssemblyName) → $(TargetPath)" /> </Target> </Project>
Keep in individual
files only what is project-specific:.csproj
<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 |
|---|---|
| Automated legacy-to-SDK conversion. Install: |
| .NET Upgrade Assistant | Full migration including API changes. Install: |
| Visual Studio | Right-click → Migrate packages.config to PackageReference |
| Manual migration | Often cleanest for simple projects — follow the checklist above |
Recommended approach:
- Run
for a first passtry-convert - 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