msbuild-modernization
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 exp
Install
npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-msbuild/skills/msbuild-modernization
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
git clone https://github.com/dotnet/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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.
<!-- 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 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" />
<!-- ... 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
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:
<!-- 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>
</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 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):
<?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.
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-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
Files (skills)
-
SKILL.md 17 KB
--- 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, <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." 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`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.