适用场景:WinForms / 类库等 .NET Framework 4.x 项目,框架版本不变(仍是 net48 / net462),只把项目文件换成新格式。
测试环境:Visual Studio 18(2026)、.NET SDK 10。

一、两种 csproj 的区别

老式 csprojSDK 式 csproj
开头<Project ToolsVersion="15.0" xmlns="..."><Project Sdk="Microsoft.NET.Sdk">
源文件每个 .cs、.resx 都要逐个列出自动包含目录下所有文件
文件长度通常上百行通常二三十行
目标框架只能是 .NET Framework.NET 8/9/10,也可以是 .NET Framework
NuGetpackages.config只支持 PackageReference
多目标不支持<TargetFrameworks>net48;net8.0</TargetFrameworks>

SDK 式说的是项目文件的格式,跟 .NET 版本无关。 项目完全可以用 SDK 式 csproj,同时继续用 .NET Framework 4.8。

二、为什么 VS 新建的项目还是老式的?

在 VS 里,项目文件的格式由模板决定:

模板名称生成的 csproj
Windows 窗体应用 (.NET Framework)老式
Windows 窗体应用(不带后缀)SDK 式,目标 .NET 8+

VS 没有现成的”SDK 式 + .NET Framework”模板,需要先用不带后缀的模板创建,再手动改目标框架。

三、新建一个 SDK 式的 .NET Framework 项目

1. 创建项目

VS 里选择 “Windows 窗体应用”(不带 .NET Framework 后缀),或者用命令行:

dotnet new winforms -n MyApp -o D:\Projects\MyApp

生成的 csproj 如下:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

2. 改成 .NET Framework 4.8

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net48</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <LangVersion>latest</LangVersion>
    <PlatformTarget>x64</PlatformTarget>
  </PropertyGroup>
</Project>

3. 修改 Program.cs

ApplicationConfiguration.Initialize() 是 .NET 6 以后才有的写法,在 net48 下要换回老写法:

[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
}

四、迁移已有的老式项目

第 0 步:备份

迁移前先提交到 git,或者把整个项目目录复制一份。迁移后有问题可以对比或还原。

第 1 步:整理旧 csproj 里的内容

打开旧 csproj,把下面这些内容摘出来,其余的大部分都可以删掉:

旧 csproj 里的内容新 csproj 怎么处理
RootNamespace、AssemblyName、OutputType保留
TargetFrameworkVersion v4.8改成 <TargetFramework>net48</TargetFramework>
PlatformTarget、LangVersion保留
<Compile Include="xxx.cs" /> 列表删掉,会自动包含
窗体的 .resx(DependentUpon)删掉,会自动关联
<Reference> 带 HintPath 的第三方 DLL保留
System、System.Core、System.Drawing 等框架引用可以先删掉,编译报缺什么再补
<ProjectReference>保留,删掉其中的 <Project>{GUID}</Project> 和 <Name>
packages.config 里的包先迁移成 PackageReference(见第 2 步)
设置了”复制到输出目录”的文件保留,改成 Update 写法(见第 3 步)
非 resx 的嵌入资源(图片等)保留
生成前/生成后事件改写成 Target(见”常见问题”)
ProjectGuid、FileAlignment、Import 系统 targets、Configuration 分组删掉

第 2 步:NuGet 包改成 PackageReference(用到 NuGet 的项目)

SDK 式项目不支持 packages.config。在 VS 里右键 packages.config,选择 “将 packages.config 迁移到 PackageReference”,迁移完成后再转换项目格式。

第 3 步:写新的 csproj

以一个 WinForms 项目为例:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net48</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <PlatformTarget>x64</PlatformTarget>
    <LangVersion>latest</LangVersion>
    <RootNamespace>MyApp</RootNamespace>
    <AssemblyName>MyApp</AssemblyName>

    <!-- 保留现有的 Properties\AssemblyInfo.cs,否则会和自动生成的程序集信息重复 -->
    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>

    <!-- 输出目录保持 bin\Debug\,不多一层 net48\ -->
    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
  </PropertyGroup>

  <!-- 第三方 DLL 引用 -->
  <ItemGroup>
    <Reference Include="halcondotnet">
      <HintPath>C:\Program Files\MVTec\HALCON-24.11-Progress-Steady\bin\dotnet35\halcondotnet.dll</HintPath>
    </Reference>
    <Reference Include="Newtonsoft.Json">
      <HintPath>..\libs\Newtonsoft.Json.dll</HintPath>
    </Reference>
    <Reference Include="System.Net.Http" />
  </ItemGroup>

  <!-- 项目引用 -->
  <ItemGroup>
    <ProjectReference Include="..\MyLib\MyLib.csproj" />
  </ItemGroup>

  <!-- 资源和设置文件的代码生成器设置,从旧 csproj 搬过来 -->
  <ItemGroup>
    <EmbeddedResource Update="Properties\Resources.resx" Generator="ResXFileCodeGenerator" LastGenOutput="Resources.Designer.cs" />
    <Compile Update="Properties\Resources.Designer.cs" AutoGen="True" DependentUpon="Resources.resx" DesignTime="True" />
    <None Update="Properties\Settings.settings" Generator="SettingsSingleFileGenerator" LastGenOutput="Settings.Designer.cs" />
    <Compile Update="Properties\Settings.Designer.cs" AutoGen="True" DependentUpon="Settings.settings" DesignTimeSharedInput="True" />
  </ItemGroup>

  <!-- 需要复制到输出目录的文件 -->
  <ItemGroup>
    <None Update="Config\app.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>

</Project>

新格式里文件已经自动包含了,所以对已有文件设置属性要用 Update,不能用 Include。用 Include 会把同一个文件再加一次,导致重复。

第 4 步:删除 obj 文件夹

老式项目留下的中间文件会干扰新格式的还原和编译,删掉 obj 文件夹。
bin 里如果放着运行时要用的数据(配方、配置文件等),不要整个删除。

第 5 步:编译验证

用 VS 打开并编译,然后逐项检查:

五、常见问题

1. 迁移后出现一堆”类型重复定义”的错误

老式项目只编译 csproj 里列出来的文件,SDK 式会编译目录下所有的 .cs 文件。如果目录里有备份副本、旧代码、以前排除掉的文件,迁移后都会被编译进去。

处理方法是删掉这些文件,或者在 csproj 里排除:

<ItemGroup>
  <Compile Remove="Backup\**" />
  <Compile Remove="Old\**" />
</ItemGroup>

2. 程序找不到 exe 旁边的文件

SDK 式默认把输出放到 bin\Debug\net48\,比以前多了一层目录。如果程序用 AppDomain.CurrentDomain.BaseDirectory 读取 exe 旁边的文件夹,就会找不到。加上下面这行即可:

<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

3. 报”重复的 AssemblyVersion 特性”(CS0579)

SDK 式默认会自动生成程序集信息,和 Properties\AssemblyInfo.cs 冲突了。有两种处理方法:

4. 报”通配符版本号与确定性生成不兼容”(CS8357)

旧代码里写了 [assembly: AssemblyVersion("1.0.*")],而 SDK 式默认开启确定性编译。可以改成固定版本号,或者在 csproj 里加:

<Deterministic>false</Deterministic>

5. 找不到 System.Windows.Forms 或其他框架类型

缺什么就补什么:

<Reference Include="System.Windows.Forms" />

6. 生成前/生成后事件不执行

旧格式的 <PostBuildEvent> 要改写成 Target:

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
  <Exec Command="xcopy /y "$(TargetPath)" "D:\Deploy\"" />
</Target>

7. 自己写的 .targets 在 csproj 末尾 Import 后不生效

老式项目可以在 Microsoft.CSharp.targets 之后 Import 自定义规则。SDK 式项目的系统编译规则是在 csproj 内容之后才隐式导入的,所以你在 csproj 里写的 Import 反而排在前面。这时 $(OutDir)、$(IntermediateOutputPath) 这些属性还没有定义,是空的。

有两种处理方法:

8. VS 自动往 csproj 里加了一些行

打开窗体设计器后,VS 可能会自动加上类似下面的内容,这是正常的:

<Compile Update="MainForm.cs">
  <SubType>Form</SubType>
</Compile>

9. 能不能用迁移工具?

可以试试 VS 扩展 .NET Upgrade Assistant,但它不一定支持最新版的 VS,生成的结果也需要逐行检查。像 WinForms 小项目这样文件不多的,手写新的 csproj 更快,也更容易掌控。

六、迁移检查清单

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注