Legacy .csproj files are hundreds of lines of XML: every source file listed explicitly (a merge conflict every time 2 people add a file), NuGet references via packages.config and HintPaths, and no support for multi-targeting. Converting to the SDK-style format is the first mechanical step of any .NET migration (see Do you have a .NET migration plan?) - and it pays off immediately, even while you're still targeting .NET Framework.
An SDK-style project starts with <Project Sdk="Microsoft.NET.Sdk"> and comes with smart defaults:
*.cs files are compiled automatically; no <Compile Include="..." /> neededOutputType defaults to Library, AssemblyName and RootNamespace default to the project name, Debug/Release configurations are built inAssemblyVersion, Company etc. are generated from MSBuild properties, replacing AssemblyInfo.cspackages.config, HintPaths, or packages folder<TargetFrameworks>net48;net10.0</TargetFrameworks> lets one project build for both frameworks during a migrationImportant: SDK-style does not mean modern .NET - an SDK-style project can still target net48. That's exactly what makes incremental migration possible: change the format first, the framework later.
Use the try-convert dotnet tool:
dotnet tool install -g try-convert
Convert while keeping your current target framework, so the format change is the only change:
try-convert --keep-current-tfms
For web projects, conversion must be forced:
try-convert --keep-current-tfms --force-web-conversion
Alternatively, the .NET Upgrade Assistant performs the conversion as part of a full upgrade - see Do you know how to modernize .NET applications?.
System.Web-based project. Converting the class libraries first and handling the web project via the incremental (side-by-side) approach is usually the right move - see Do you know how to migrate Web Apps to .NET?<UseWindowsForms>true</UseWindowsForms> or <UseWPF>true</UseWPF>, and when you later add a modern target framework use net10.0-windows - see Do you know how to multi-target your .NET projects?try-convert migrates it to PackageReference. Afterwards, only direct dependencies should remain - transitive packages no longer need to be listedConversion tools err on the side of caution, so converted .csproj files usually keep legacy leftovers. The whole point of SDK-style projects is that they are small enough to read in one screen and rarely cause merge conflicts - leftover cruft throws those benefits away.
After conversion, go through each project and remove:
<Compile> items that just re-include what the glob already coversOutputType=Library, AssemblyName/RootNamespace equal to the project name, empty Debug|Release PropertyGroup blocksProjectGuid, ProjectTypeGuids, SchemaVersion, AppDesignerFolder, FileAlignment<Import Project="...Microsoft.CSharp.targets" /> and packages.config-era .props/.targets imports (the SDK handles these)Reference items with HintPaths into a packages folder - replace with PackageReferenceAssemblyInfo.cs - delete it and move any meaningful values (company, product, InternalsVisibleTo) into the csproj or Directory.Build.props. If you keep it, set <GenerateAssemblyInfo>false</GenerateAssemblyInfo> or the build fails with CS0579: Duplicate 'AssemblyVersion' attributeThen pull the properties that are identical across projects (target framework, nullability, analyzers) up into a Directory.Build.props, and manage package versions with Central Package Management.
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><ProjectGuid>{F2A71F9B-5D33-465A-A702-920D77279786}</ProjectGuid><OutputType>Library</OutputType><RootNamespace>Northwind.Domain</RootNamespace><AssemblyName>Northwind.Domain</AssemblyName><TargetFramework>net10.0</TargetFramework><FileAlignment>512</FileAlignment></PropertyGroup><PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'"><DebugSymbols>true</DebugSymbols><Optimize>false</Optimize><OutputPath>bin\Debug\</OutputPath></PropertyGroup><ItemGroup><Compile Include="Entities\Customer.cs" /><Compile Include="Entities\Order.cs" /><Compile Include="Properties\AssemblyInfo.cs" /></ItemGroup></Project>
❌ Bad example - Converted but never cleaned: every line here is either a default or legacy noise
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net10.0</TargetFramework></PropertyGroup></Project>
✅ Good example - The same project after cleanup (with shared properties in Directory.Build.props)
Converting and cleaning up csproj files is exactly the kind of change that shouldn't alter build output - prove it:
bin\