Skip to main content
skill CX Codex

msbuild-modernization — Claude Code skill: .csproj to SDK-style

A skill that rewrites legacy .NET project files to SDK-style: trims the XML, moves packages to PackageReference, hands assembly attributes to the SDK.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add dotnet/skills --skill msbuild-modernization -a claude-code
Open on GitHub

What msbuild-modernization does

The skill takes an inherited .csproj or .vbproj and turns it into an SDK-style project. It starts by looking for the usual tells: a ToolsVersion attribute, an Import of Microsoft.CSharp.targets through $(MSBuildToolsPath), a packages.config, a Properties\AssemblyInfo.cs. Quick check before you commit to the work: a simple library whose project file runs past 50 lines is almost certainly legacy.

Then it walks the migration. The root element becomes <Project Sdk="Microsoft.NET.Sdk">, TargetFrameworkVersion becomes TargetFramework (v4.7.2 → net472), and the <Compile Include> and <Content Include> groups are deleted outright because the SDK globs files on its own. Assemblies attributes leave AssemblyInfo.cs for project properties; packages.config becomes <PackageReference> items.

Keeping AssemblyInfo.cs is possible, but then the project needs <GenerateAssemblyInfo>false</GenerateAssemblyInfo>. Without it the attributes get generated twice and the build fails. The skill also stays away from projects already in SDK-style, from npm, Maven and CMake builds, and from .NET Framework projects that cannot make the move.

What it helps with

  • convert a legacy .csproj to SDK-style without hand-editing XML
  • move NuGet packages out of packages.config into PackageReference
  • strip long <Compile Include> lists from a project
  • move assembly attributes from AssemblyInfo.cs into project properties
  • consolidate shared settings into Directory.Build.props

Example prompts for the agent

  • Convert MyLibrary.csproj to SDK-style and show me the diff
  • Migrate every packages.config in this solution to PackageReference
  • Pull the shared settings out of the projects into Directory.Build.props

How to install msbuild-modernization

1Skills CLI (Vercel)

npx skills add dotnet/skills --skill msbuild-modernization -a claude-code

2Manually: put the files in place

~/.claude/skills/msbuild-modernization/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/msbuild-modernization/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about msbuild-modernization

How do I install msbuild-modernization in Claude Code?

Run npx skills add dotnet/skills --skill msbuild-modernization -a claude-code. The skill lands at ~/.claude/skills/msbuild-modernization/SKILL.md.

What does msbuild-modernization do?

It migrates legacy .csproj and .vbproj files to SDK-style step by step: drops ToolsVersion, .targets imports and explicit file lists, swaps TargetFrameworkVersion for TargetFramework, converts packages to PackageReference.

Does it work on .NET Framework projects?

Yes, as long as the project can move to SDK-style; the mapping covers v4.6.1, v4.7.2 and v4.8. Projects that cannot make that move are out of scope.

Source text

SKILL.md · MIT
---
name: msbuild-modernization
description: "Guide for modernizing and migrating MSBuild project files to SDK-style format. 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, an Import of Microsoft.CSharp.targets via $(MSBuildToolsPath), .csproj files over 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."
license: MIT
---

# 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 `.cs` file)
- `ToolsVersion` attribute on `<Project>` element
- `packages.config` file present
- `Properties\AssemblyInfo.cs` with 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 of `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.

```xml
<!-- 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>
```

```xml
<!-- 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
<?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:**

```xml
<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:**

```xml
<PropertyGroup>
  <TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
</PropertyGroup>
```

**AFTER:**

```xml
<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:**

```xml
<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:

```xml
<ItemGroup>
  <Content Include="..\shared\config.json" Link="config.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```

### Step 4: Remove AssemblyInfo.cs

**BEFORE** (`Properties\AssemblyInfo.cs`):

```csharp
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`):

```xml
<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:

```xml
<PropertyGroup>
  <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
```

### Step 5: Migrate packages.config → PackageReference

**BEFORE** (`packages.config`):

```xml
<?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`):

```xml
<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 `<runtime>` section from `app.config` if present

### Step 6: Remove Unnecessary Boilerplate

Delete all of the following — the SDK provides sensible defaults:

```xml
<!-- 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:

```xml
<PropertyGroup>
  <TargetFramework>net8.0</TargetFramework>
  <Nullable>enable</Nullable>
  <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
```

- `<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 with `global.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):

```xml
<?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):

```xml
<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:

```xml
<ItemGroup>
  <EmbeddedResource Include="..\shared\Schemas\*.xsd" LinkBase="Schemas" />
</ItemGroup>
```

**Content files with CopyToOutputDirectory:** these still need explicit entries:

```xml
<ItemGroup>
  <Content Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  <None Include="scripts\*.sql" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
```

**Multi-targeting:** change the element name from singular to plural:

```xml
<!-- Single target -->
<TargetFramework>net8.0</TargetFramework>

<!-- Multiple targets -->
<TargetFrameworks>net472;net8.0</TargetFrameworks>
```

**WPF/WinForms projects:** use the appropriate SDK or properties:

```xml
<!-- 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:

```xml
<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](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`:

```xml
<!-- 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.

**`Directory.Build.props`** (for properties — placed at repo or src root):

```xml
<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):

```xml
<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:

```xml
<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:**

1. Run `try-convert` for a first pass
2. Review and clean up the output manually
3. Build and fix any issues
4. Enable modern features (nullable, implicit usings)
5. Consolidate shared settings into `Directory.Build.props`

© dotnet · MIT · dotnet/skills

Similar skills

Everything for Backend and APIs →

From the same set101

msbuild-antipatterns Audits MSBuild project files and targets to surface anti-patterns. Helpful when reviewing .csproj and .fsproj files for maintainability without unwanted automated edits. CX Codex +1 3,900installs code-testing-agent A compatibility alias for code-testing that forwards explicit legacy calls to the current code-testing workflow. CX Codex +1 4,000installs csharp-scripts Runs C# from a single file with dotnet, no project needed. Useful for testing a language feature or building a small utility. CX Codex +1 3,800installs dotnet-trace-collect Guides developers in selecting diagnostic tools and capturing traces for production .NET performance issues across Windows, Linux, and containers. CX Codex +1 3,800installs directory-build-organization A skill that decides what belongs in Directory.Build.props, .targets and .rsp, so several projects stop repeating the same MSBuild policy. CX Codex +1 3,700installs coverage-analysis Interpret .NET line, branch, and condition coverage, find gaps, and diagnose plateaus. Use it to examine Cobertura reports or assess project-wide refactoring risk. CX Codex +1 3,600installs binlog-generation The skill makes your agent add /bl:{} to every MSBuild command, so each build leaves a .binlog with a full trace you can inspect after a failure or a slowdown. CX Codex +1 3,500installs dotnet-pinvoke Calls C and C++ native libraries from .NET via DllImport and LibraryImport. Helps map signatures accurately, configure string marshalling, and prevent memory corruption. CX Codex +1 3,500installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor