适用场景:WinForms / 类库等 .NET Framework 4.x 项目,框架版本不变(仍是 net48 / net462),只把项目文件换成新格式。
测试环境:Visual Studio 18(2026)、.NET SDK 10。
一、两种 csproj 的区别
| 老式 csproj | SDK 式 csproj | |
|---|---|---|
| 开头 | <Project ToolsVersion="15.0" xmlns="..."> | <Project Sdk="Microsoft.NET.Sdk"> |
| 源文件 | 每个 .cs、.resx 都要逐个列出 | 自动包含目录下所有文件 |
| 文件长度 | 通常上百行 | 通常二三十行 |
| 目标框架 | 只能是 .NET Framework | .NET 8/9/10,也可以是 .NET Framework |
| NuGet | packages.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>
LangVersion:net48 默认只能用 C# 7.3,而模板里的namespace X;写法和隐式 using 都需要更新的语言版本。PlatformTarget:用到 x64 原生库(比如 HALCON、相机 SDK)时要指定。ImplicitUsings在 net48 下也能用,编译时会自动生成以下全局 using:System、System.Collections.Generic、System.Drawing、System.IO、System.Linq、System.Threading、System.Threading.Tasks、System.Windows.Forms。
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 打开并编译,然后逐项检查:
- 编译没有错误
bin\Debug\里的文件和迁移前一样,exe 的位置没变- 程序能正常运行,读取配置、数据的功能正常
- 窗体设计器能打开每个窗体
- Release 配置也能编译
五、常见问题
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 冲突了。有两种处理方法:
- 保留 AssemblyInfo.cs:在 csproj 里加
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> - 删掉 AssemblyInfo.cs:把版本号移到 csproj 里,写成
<Version>1.2.3</Version>
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) 这些属性还没有定义,是空的。
有两种处理方法:
- 把 Import 放进项目目录下的
Directory.Build.targets文件里,它会在系统规则之后导入:<Project> <Import Project="..\build\MyRules.targets" /> </Project> - 修改 .targets 本身:把依赖这些属性的
PropertyGroup挪到 Target 内部,让它在编译时再计算。
8. VS 自动往 csproj 里加了一些行
打开窗体设计器后,VS 可能会自动加上类似下面的内容,这是正常的:
<Compile Update="MainForm.cs">
<SubType>Form</SubType>
</Compile>
9. 能不能用迁移工具?
可以试试 VS 扩展 .NET Upgrade Assistant,但它不一定支持最新版的 VS,生成的结果也需要逐行检查。像 WinForms 小项目这样文件不多的,手写新的 csproj 更快,也更容易掌控。
六、迁移检查清单
- 迁移前已备份或提交 git
packages.config已迁移成PackageReferenceTargetFramework写成net48(或原来的版本)- 加了
UseWindowsForms/PlatformTarget/LangVersion - 处理了 AssemblyInfo:
GenerateAssemblyInfo=false或删掉文件 - 需要时加上
AppendTargetFrameworkToOutputPath=false - 第三方 DLL 引用的
HintPath路径有效 - “复制到输出目录”的文件用
Update写法保留 - 生成事件已改写成 Target
- 排除了目录里不该编译的旧
.cs文件 - 删除了
obj文件夹 - Debug / Release 都能编译,程序和窗体设计器都正常