Guide for modernizing and migrating MSBuild project files to SDK-style format. Only activate in MSBuild/.NET build context. USE FOR: converting legacy…
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.
<!-- 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-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:
<!-- 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 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 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-convert for 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.propsdon't have the plugin yet? install it then click "run inline in claude" again.
restructured original into implexa six-component format, added explicit decision points for wpf/winforms/multi-target/central package management edge cases, documented tools and validation steps, enforced voice guidelines, removed em-dashes.
migrate legacy .net framework msbuild projects to modern sdk-style format. use this when you're stuck on .net framework 4.x with verbose 80+ line .csproj files, explicit file includes, packages.config, and properties/assemblyinfo.cs boilerplate. sdk-style projects are 10-15 lines, auto-generate assembly metadata, use implicit globbing, and unblock you to .net 5+. activate only in msbuild/.net build context.
dotnet tool install -g try-convert)identify legacy format: check if .csproj contains <Project ToolsVersion=, <Import Project="$(MSBuildToolsPath), <TargetFrameworkVersion>, or explicit <Compile Include= blocks for every .cs file. if more than 50 lines for a simple library or console app, it is legacy.
replace project root element: delete xml declaration, ToolsVersion attribute, xmlns namespace, and both <Import Project= statements (Microsoft.Common.props and Microsoft.CSharp.targets). replace with single line: <Project Sdk="Microsoft.NET.Sdk">. input: old root element. output: new root element with Sdk attribute only.
migrate targetframework: convert TargetFrameworkVersion to TargetFramework using mapping (v4.6.1 → net461, v4.7.2 → net472, v4.8 → net48; or upgrade to net6.0, net8.0). input: <TargetFrameworkVersion> element. output: <TargetFramework> element with sdk-style tfm.
remove explicit file includes: delete all <ItemGroup> blocks containing <Compile Include=, <Content Include=, or <EmbeddedResource Include= entries for standard source files in project directory (*.cs, *.xaml, *.cshtml, etc.). sdk-style projects auto-glob these. input: multiple <ItemGroup> blocks. output: deleted blocks. exception: keep explicit entries only for files outside project directory (use Link= attribute) or with special metadata (e.g., CopyToOutputDirectory="PreserveNewest").
remove assemblyinfo.cs: delete properties/assemblyinfo.cs file. move assembly-level attributes to .csproj <PropertyGroup>: <AssemblyTitle>, <Description>, <Company>, <Product>, <Copyright>, <Version>. input: properties/assemblyinfo.cs. output: deleted file, properties added to .csproj. alternative: if you must keep assemblyinfo.cs, set <GenerateAssemblyInfo>false</GenerateAssemblyInfo> in .csproj.
migrate packages.config to packagereference: convert each <package> entry in packages.config to <PackageReference Include="..." Version="..." /> in .csproj. delete packages.config file. input: packages.config xml. output: .csproj ItemGroup with PackageReference elements. option: use visual studio right-click migrate or dotnet migrate-packages-config tool for automation.
remove binding redirects: if app.config or web.config contains <runtime><assemblyBinding> section with binding redirects, delete it. sdk-style projects auto-generate binding redirects at build time. input: app.config/web.config. output: cleaned config files without runtime section.
delete boilerplate: remove all <PropertyGroup Condition= blocks for Configuration/Platform (Debug|AnyCPU, Release|AnyCPU) because sdk defaults match standard settings. remove <ProjectGuid>, <AppDesignerFolder>, <FileAlignment>, <AutoGenerateBindingRedirects>, <Deterministic>. remove <ItemGroup> with framework assembly references (System, System.Core, System.Data, System.Xml, Microsoft.CSharp). remove <None Include="packages.config" />. remove <Service Include= entries. input: boilerplate property/item groups. output: deleted groups, .csproj reduced to 11-15 lines.
enable modern features (optional but recommended): add to <PropertyGroup>: <Nullable>enable</Nullable> for nullable reference type analysis, <ImplicitUsings>enable</ImplicitUsings> to auto-import common namespaces (.net 6+). do not add <LangVersion>latest (causes inconsistent builds across machines); either omit it entirely or pin numeric version (e.g., <LangVersion>12</LangVersion>) if reproducibility is critical. use global.json at repo root to pin sdk version repo-wide instead. input: empty PropertyGroup or existing one. output: PropertyGroup with nullable and implicit usings enabled.
consolidate shared properties (multi-project repos): create Directory.Build.props at repo or src root with common <PropertyGroup> entries (TargetFramework, Nullable, ImplicitUsings, Company, Copyright, TreatWarningsAsErrors, etc.). each .csproj inherits these automatically. remove duplicate entries from individual .csproj files. input: repeated properties across multiple .csproj files. output: Directory.Build.props with shared settings, individual .csproj files with only project-specific entries.
build and validate: run dotnet build on migrated project. fix any compilation errors (usually missing using statements if you enabled ImplicitUsings without manually importing). run unit tests if present. verify output binaries in bin/Debug or bin/Release. input: migrated .csproj. output: clean build, passing tests, binaries in expected location.
if project uses packages.config: migrate to PackageReference (step 6). if you run try-convert tool, it does this automatically.
if project has explicit file includes for standard locations (e.g., Models/, Services/, Controllers/ directories): delete them entirely (step 4). sdk implicit globbing finds all *.cs files under project root.
if project has files outside project directory (e.g., ../shared/config.json): keep explicit <Content Include= or <Compile Include= entries with Link= attribute to map them into project virtual structure (step 4 exception).
if project has content files requiring special output behavior (e.g., appsettings.json must copy to output directory): keep explicit <Content> or <None> entries with CopyToOutputDirectory="PreserveNewest" or CopyToOutputDirectory="Always" (step 8 exception).
if project is wpf or winforms: use <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> instead of standard SDK, or use standard SDK with <UseWPF>true</UseWPF> or <UseWindowsForms>true</UseWindowsForms> properties.
if project is a test project: use standard SDK with <IsPackable>false</IsPackable> property to prevent accidental nuget packaging. add test framework packages (xunit, nunit, mstest) as PackageReference.
if project targets multiple frameworks (e.g., .net 4.7.2 and .net 6.0): use <TargetFrameworks>net472;net6.0</TargetFrameworks> (plural) instead of singular TargetFramework.
if repo uses central package management: create Directory.Packages.props at repo root with <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally> and <PackageVersion> items for all packages. remove Version attribute from individual project PackageReference entries.
if assemblyinfo.cs contains custom attributes beyond standard ones (e.g., InternalsVisibleTo, CLSCompliant): either move them to .csproj as properties where supported, or keep the file and set <GenerateAssemblyInfo>false</GenerateAssemblyInfo>.
if app.config/web.config has binding redirects: delete them (step 7). if build fails due to assembly version mismatches, the issue is usually a transitive dependency problem, not missing redirects. review nuget package versions.
migrated .csproj file: 10-20 lines, <Project Sdk="Microsoft.NET.Sdk"> at root, single <TargetFramework> (or <TargetFrameworks> for multi-target), <PropertyGroup> with only essential metadata (RootNamespace, AssemblyName, OutputType if not Library, Version/Description/Company if needed), one or more <ItemGroup> with PackageReference and ProjectReference entries only, no boilerplate Import or assembly-reference blocks.
deleted files: packages.config, properties/assemblyinfo.cs, binding redirect sections in app.config/web.config.
new files (optional): Directory.Build.props at repo root if consolidating shared properties across multiple projects.
build output: dotnet build succeeds with no errors, binaries appear in bin/Debug/net472 (or appropriate tfm) or bin/Release.
no breaking changes: existing nuget package versions preserved, assembly name and namespace unchanged, binary output behavior identical to legacy build.
you know it worked when: dotnet build runs clean with zero errors. if you enabled ImplicitUsings and get "type not found" errors, add using statements manually (one-time). binaries in bin/Debug and bin/Release match pre-migration behavior in size and functionality. if you have tests, they pass. visual studio intellisense works without project-reload prompts. ci/cd pipeline builds the migrated project without modification (or with minimal env var updates). you can now upgrade TargetFramework to net6.0, net8.0, or later without re-doing this migration.
quick validation: open migrated .csproj in editor, count lines (target: under 25 for simple library). confirm no <Import Project=, no <ToolsVersion, no <TargetFrameworkVersion, no explicit <Compile Include= blocks for *.cs files. run dotnet build -c Debug && dotnet build -c Release and confirm both succeed. check bin/ directory structure matches old behavior.