
1. 先别急着动手搞懂这次升级到底在升什么我接过不少老项目几乎每次听到“把 ASP.NET MVC 升级到 .NET”这句话第一反应都是先按住对方的手别急着开 IDE。因为这句话本身就有歧义你手里那个跑在 .NET Framework 4.x 上的 ASP.NET MVC 5 项目和我说的“.NET 6/7/8”平台根本是两个时代的东西。很多团队死磕了几个星期最后发现双方连目标都没对齐。先说清楚这次的升级逻辑。ASP.NET MVC 是一个 Web 框架它属于旧版 .NET Framework 时代自带 System.Web 那一套跑在 Windows 的 IIS 上。而现在的 .NET.NET Core 之后统称 .NET 5/6/7/8是一个跨平台运行时搭配的 ASP.NET Core MVC 是彻底重写过的框架。所以“升级”本质上包含了两层底层运行时从 .NET Framework 换到 .NET上层框架从 ASP.NET MVC 换成 ASP.NET Core MVC。这两层必须一起换没有只换一半的选项。我见过有人试图先升级到 .NET Framework 4.8再慢慢改代码这种做法不能说完全没用它确实能清理一部分 API 层面的兼容性问题但整体思路如果还是 System.Web那离真正的现代化还有十万八千里。我的建议是直接以 .NET 8 为目标现在 .NET 8 是 LTS 版本支持到 2026 年 11 月比 .NET 7 那种非 LTS 稳妥得多一口吃成胖子虽然后期痛苦但至少方向不会跑偏。这篇文章不是理论科普而是我实打实从一堆老项目中趟出来的升级路径。从评估、准备、改造、排坑到上线验证每一步都有据可查。如果你手里也有一个跑了五六年的 MVC 老项目被老板要求“提升性能”“上容器”“要跨平台”又或者只是因为 .NET Framework 4.x 的官方支持即将结束被迫迁移那这篇内容就是给你准备的。我不打算给你画一个轻飘飘的“升级很简单”的大饼恰恰相反真实的过程涉及大量琐碎工作项目文件格式重构、配置系统替换、依赖注入改造、身份认证机制重写、第三方库替换、性能调优……每一块都会出问题只是多少的问题。把预期放低才能把过程做扎实。2. 升级之前的那道选择题原地迁移还是渐进式重构很多人拿到老项目第一个动作就是创建一个新的 .NET 8 项目然后把旧代码往里面拷贝。这个思路没有错但先别急着敲键盘因为迁移策略直接决定了你这几个月的痛苦程度。我总结下来主流方案有三个适合不同情况的项目。第一个方案整体迁移Big Bang。把整个解决方案一次性迁移到新平台所有项目全部转换然后在分支上改到能编译、能运行、能过测试最后再合并进主干。这个方案适合项目规模不大比如核心就是两三个项目、代码量在几十万行以内、团队人数不多、没有并行迭代压力的场景。优点是干净缺点是周期长通常 2 到 4 周起步期间新的需求完全冻结如果产品经理在这时候塞需求进来你会崩溃。第二个方案渐进式并行Strangler Pattern。保留旧系统继续跑新系统在老系统旁边慢慢长出来通过反向代理或者负载均衡把一部分请求导流到新系统验证通过后逐步扩大流量比例直到老系统退居幕后。这个方案适合项目非常大、不可能一次性迁移完成、又没法接受长时间冻结业务的场景。很多大公司实际都是这么干的。缺点是技术门槛高你需要同时维护两套系统的部署、数据同步、会话共享复杂度直接翻倍。第三个方案先做模块解耦再逐模块迁移。把老项目里的核心业务逻辑和表现层分开比如把数据处理的部分抽成独立的类库在新平台重新实现 Web 层通过接口调用方式把老代码绑进来。这个方案适合团队想反复试水、不想一次梭哈的情况但注意它不适合项目结构本身就像一团乱麻的情况——如果连模块边界都划不清解耦只是把你的痛苦分摊到更多代码里。就我自己的经验大部分人遇到的场景其实是第一种和第二种的组合项目不算小但也没大到必须长期并行团队有几个人但都不熟悉新平台。这种情况下我会建议你走一条“模拟整体迁移”的路线——在一个独立的分支里花两周时间把整个解决方案强制迁移到能编译能跑不要管细节完美不完美先把主要问题全部暴露出来。这一步的意义是让你拿到一份“问题清单”哪些类库没有对应版本哪些 API 不好替换哪些第三方组件是坑。有了这份清单后面的决策才有依据。我的习惯是做这次评估的时候会拉一个表格把解决方案里每个项目单独一行标注它的类型Web 应用、类库、测试项目、依赖的关键 NuGet 包、以及预估迁移难度低/中/高。别小看这张表它会成为你整个迁移期间的作战地图。下表是我帮一个朋友做迁移评估时实际用过的模板你可以参考项目类型关键依赖迁移难度主要风险AdminSiteASP.NET MVC 5Entity Framework 6, Autofac中DI 替换、EF 升级Core.Domain类库无低目标框架修改Core.Services类库System.Web注意高HttpContext 引用Legacy.Utils类库Newtonsoft.Json低基本无TestsMSTest无中测试框架迁移至 xUnit/NUnit等你把这张表填完迁移的整体工作量就浮出水面了。接下来就进入实操阶段。3. 环境准备别用老 IDE 干新活的三个理由很多人在第一步就栽了跟头用 Visual Studio 2015 打开 .NET 8 项目报错看不懂改无可改。迁移到新平台开发环境必须跟着升级这不是矫情是硬性要求。以我的推荐配置来说Visual Studio 202217.8 或更高版本是 Windows 上的首选。它内置了 .NET 8 SDK 的工具链支持新版本的项目文件格式SDK-style、热重载、以及基于 HTTP 的调试体验。如果你用的是 Mac 或者 Linux那么 Visual Studio Code C# Dev Kit 也能用得很顺手命令行构建配合dotnetCLI 完全够用。很多老代码只能跑在 .NET Framework 的地方比如某些遗留 WebForms 项目在 VS2022 里可以用“条件编译”的方式兼容不过那是另一个话题了这里不展开。至于 .NET SDK 版本我的建议是直接装.NET 8.0 SDK别贪新用预览版做生产迁移。LTS 版本的稳定性意味着你不会在迁移过程中被运行时或编译器的 bug 打断。在命令行里验证一下环境是否就绪dotnet --version # 你应当看到类似 8.0.100 或更高版本的数字除了工作机环境还有一个我看大家容易忽略的源码管理分支。在动手之前建议你从主干拉一条feature/migration-to-net8分支出来并且在迁移期间所有提交都只进这条分支不允许直接合入主干。这不是流程控而是万一中途要做紧急 bug 修复主干还能正常出版本不会因为迁移搞到一半导致整个团队没法干活。我见过因为迁移分支污染了主干最后只能回滚的惨案别重蹈覆辙。最后一个是代码分析工具。老项目里往往积累了大量的编译警告和过时代码迁移之前我会先跑一遍静态分析比如 Visual Studio 自带的代码分析或者 NuGet 包分析器把这些警告导出一个基线清单。迁移完成后对比这个清单能帮你验证迁移是否在“行为不变”的前提下完成。很多人不做这一步导致迁移后代码虽然能跑但隐藏问题一堆上线后靠 bug 来测试。4. 核心迁移步骤从项目文件、配置系统到入口程序的完整替换4.1 第一步把 .csproj 从“老古董”格式换成 SDK 风格这一步是最机械、但也最容易出岔子的。旧版 .NET Framework 的 .csproj 文件是那种好几百行的 XML里面到处是Reference标签、条件编译符号、以及一堆自动生成的 GUID 和导入语句。SDK 风格的项目文件只有几行到几十行靠通配符自动引入所有 .cs 文件靠PackageReference管理 NuGet 依赖。我见过有朋友尝试手动把旧 .csproj 改写成新格式结果改了半天下半身GUID 搞丢了需要重新添加项目引用。我的建议是不手动改而是直接新建 SDK 风格的项目文件然后把旧代码文件搬进来。具体操作如下在解决方案里新建一个类库项目目标框架选net8.0。将原来项目里的所有.cs文件复制进新项目目录SDK 风格会自动包含不需要手动添加。将App.config/Web.config中的连接字符串等配置迁移到新项目的appsettings.json。在 NuGet 管理里重新添加原本的依赖包能用新版本的就用新版本见下方第 4.2 节。删除旧项目文件把新项目文件重命名为旧项目的名字。对于 Web 项目本身因为 ASP.NET Core MVC 的项目结构和 ASP.NET MVC 差异太大没有 Global.asax、没有 Controller 列表配置、没有 App_Start 文件夹所以 Web 项目通常不是“改造”而是“新建 把 Controller / View / Model 搬过来”。下面会专门讲。4.2 第二步NuGet 包的替换优先级与兼容性排查NuGet 包是迁移过程中最磨人的环节。老项目里躺着各种老版本包有些在新平台上有对应版本有些没有有些换了新名字。我的经验是这样的改动最小的Newtonsoft.Json、AutoMapper、NLog 这类与运行时关系不大的包通常有 netstandard2.0 或 net6.0 版本直接升级版本即可。需要换替代品的有些不兼容 System.Web 的包比如跨域配置包、某些基于 OWIN 的中间件可能需要换成 ASP.NET Core 原生中间件。直接淘汰的一些老式工具类库比如替换 web.config 里httpModules的包、依赖 AppDomain 的插件机制这些在新平台根本没有对应 API只能重写相关逻辑不能用“降级”手段硬扛。一个很实用的排查技巧在 NuGet 包里找该库的TargetFramework支持情况。如果它包含netstandard2.0或netcoreapp3.1以上的目标大概率可以直接用。如果只有net461或netstandard1.x这种老目标那就需要谨慎评估很可能会有运行时兼容问题。这里有一个我踩过的大坑某个日志组件在旧项目里用的是 1.x 版本写了大量自定义 Appender迁移后发现新版本框架完全变了Appender 那一套已经被移除改用ILoggerProvider。这意味着我不仅要升级包还要重写那段自定义日志代码。这种隐藏工作量往往被团队在评估时完全忽略建议你在做工作量评估时按 1.5 到 2 倍的系数去打余量。4.3 第三步Startup 类与新入口——替换 Global.asax 和 App_StartASP.NET MVC 老项目的核心入口是Global.asax里面挂着Application_Start()然后到哪里调用什么“Config”方法比如AreaRegistration.RegisterAllAreas()、FilterConfig.RegisterGlobalFilters()、RouteConfig.RegisterRoutes()。这些代码在 ASP.NET Core 里不存在了取而代之的是Program.cs.NET 6 简化模板里的WebApplicationBuilder和WebApplication。转换对应关系大概是这样老概念新概念Global.asax / Application_StartProgram.cs 里的 builder 配置与 app 构建RouteConfigapp.MapControllerRoute()或属性路由FilterConfig全局过滤器builder.Services.AddControllers(...options ...)里的 Filter 配置BundleConfig打包压缩前端构建工具或静态中间件建议直接换前端方案Unity / Autofac 容器builder.Services自带的依赖注入容器或保留 Autofac 作为第三方 ProviderWeb.config transformappsettings.json 环境变量搭配 launchSettings.json举个例子老项目里你大概写过类似这样的路由注册代码public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute({resource}.axd/{*pathInfo}); routes.MapRoute( name: Default, url: {controller}/{action}/{id}, defaults: new { controller Home, action Index, id UrlParameter.Optional } ); }在 ASP.NET Core 里等价物是 Program.cs 里的这段var builder WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); var app builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.MapControllerRoute( name: default, pattern: {controllerHome}/{actionIndex}/{id?}); app.Run();你可能会说“这不是差不多吗”对概念上确实相似但底层差别巨大新路由系统基于 endpoints支持属性路由混合、支持中间件管道管道顺序控制而且不再依赖 System.Web 的 Request/Response。接下来进入真正的改造核心。4.4 第四步Controller、View 和 Model 的搬迁工作流Controller 的迁移算是整个过程中最“友好”的一段因为 ASP.NET Core MVC 在 API 层面与老 MVC 高度相似。但有几个细节必须处理Controller 基类老项目里Controller类来自System.Web.Mvc新项目里改成Microsoft.AspNetCore.Mvc。这个 namespace 变化牵涉到所有 using如果 Controller 文件多建议批量替换而不是手动改。返回类型ActionResult依然存在但像FileResult、JsonResult这些的实现细节已经变了。特别是Json()方法老 MVC 返回的是JavaScriptSerializer或 Newtonsoft.Json 序列化结果新框架默认用 System.Text.Json虽然在 .NET 8 里你可以通过配置切回 Newtonsoft.Json。这个变化会导致 JSON 序列化大小写风格不同前端如果对字段名有依赖必须注意。异步老 MVC 的asyncaction 已经支持但 ASP.NET Core 里对异步的支持更加彻底。如果你有一些同步的 action迁移时建议顺便改为 async牵扯到 EF 操作的话几乎是必须的下面详谈。依赖注入老 MVC 里 Controller 可以直接new或通过容器解析新框架里 Controller 的构造函数里直接用构造函数注入即可框架自动管理生命周期。View 这边.cshtml文件本身基本不需要大改但注意Web.config里那些针对 Razor 的配置比如system.web.webPages.razor不再生效默认配置就能跑大部分场景。布局页_Layout.cshtml里如果有用Scripts.Render或Styles.Render这类 Bundle 辅助方法这些已经不存在了需要自己写script/link标签或者换用前端自动化构建方案。另外老项目里常用的Session、ViewBag、TempData这些依然支持但TempData默认实现从使用Session变成了基于Cookie可以配置但行为有差异如果你依赖 TempData 跨请求传大体积数据性能会有变化需要注意。Model 层即你定义业务实体的那些类通常迁移难度最低主要是命名空间替换以及对DataAnnotations和Json.NET属性的调整。如果项目用了 Entity Framework 6需要评估是继续用 EF6它兼容 .NET Core 3.1可运行在现代 .NET 上但效果并非完美还是升级到 EF Core 8。我建议直接升 EF Core因为 EF6 在新平台上的运行支持文档自己都说“优先使用 EF Core”而且 EF Core 的 LINQ 翻译能力比 EF6 强太多比如GroupBy翻译、Take的 SQL 生成。但注意 EF Core 的 Fluent API 配置变化不小从modelBuilder.Configurations.Add到modelBuilder.ApplyConfigurationsFromAssembly年代感扑面而来。下面我会专门把 EF6 到 EF Core 的迁移单拎出来因为它太能暴露问题了。5. 数据库访问层的迁移EF6 到 EF Core 的真实差异和应对手段如果说把 Controller 和 View 从一个项目复制到另一个项目是 3 小时工作量那么数据访问层的迁移很可能就是 3 天。因为老项目里的数据访问代码不会只用基础 CRUD往往还夹着一堆“高级技巧”延迟加载、多对多映射、略复杂的 LINQ 查询、还有因为 EF6 翻译不完整被迫用AsEnumerable()的骚操作。先列一个对比表方便你心里有底维度EF6EF Core 8DbContext 注册手动实例化或旧 DI 容器builder.Services.AddDbContextUseSqlServer链式配置延迟加载默认开启virtual 导航属性默认关闭需安装Microsoft.EntityFrameworkCore.Proxies包并在注册时UseLazyLoadingProxies()LINQ 翻译能力较弱复杂查询经常拉全表到内存翻译能力强很多但极少数情况仍有客户端评估提醒迁移命令Enable-Migrations/Update-Database等dotnet ef migrations add/dotnet ef database update配置映射[Table]特性和 Fluent API 混用推荐全部用 Fluent API 的IEntityTypeConfigurationT类批量更新删除不支持需先查后删支持ExecuteUpdate/ExecuteDelete.NET 7性能大幅提升事务Database.BeginTransaction同样支持但IDbContextTransaction命名空间不同迁移过程中的第一个坑就是延迟加载。老项目里如果你习惯在 Controller 里访问某个实体的导航属性比如customer.Orders靠的其实是 EF6 默认的延迟加载。到了 EF Core 这里默认关闭了如果你不重新开启或者不写Include显式加载你只会在运行时得到 null 引用。这个问题在开发时不一定立刻暴露因为单个对象还好但当你遍历一个列表时瞬间 N1 查询让你慢到怀疑人生。我处理这个问题的套路是强制开启延迟加载如果你不想大规模修改查询代码但在业务代码层面明确它只是临时方案长期还是要改成Include或用专门的数据加载策略。因为在 EF Core 下开延迟加载意味着额外开代理是整个框架里性能最差、最容易踩坑的选项之一。但迁移阶段为了快速恢复“行为等价”这个妥协是可以容忍的。第二个坑是表达式树的翻译差异。EF6 里有些查询能跑到 EF Core 里刚跑就报“could not be translated”或者反过来。比如在谓词里调用自定义方法EF6 会对本地方法静默处理实际上是客户端评估EF Core 会直接拒绝要求你先AsEnumerable()再过滤或者换成可翻译的写法。说实话我更认可 EF Core 的严格态度因为客户端评估会让数据量一上来就彻底瘫痪老项目就是这么被拖慢的。遇到这种查询建议你直接停下来思考业务意图重写查询而不是绕过。第三个问题是迁移命令的变化。老项目里环境可能就是 VS 里的 Package Manager Console敲Add-MigrationUpdate-Database新项目里你要用命令行工具dotnet tool install --global dotnet-ef dotnet ef migrations add InitialMigration dotnet ef database update这个dotnet-ef工具的版本必须和Microsoft.EntityFrameworkCore.Design包版本匹配否则容易报版本冲突。另外迁移脚本里如果包含自定义 SQL比如存储过程、视图这些不会自动迁移需要你手工导入到数据库并在代码里做好对应的映射或调用。6. 中间件管道和配置系统的替换最容易翻车的地方到一个新平台最容易被忽略的就是请求处理管道的差异。老 ASP.NET 有一套基于 HTTP Module 和 HTTP Handler 的扩展机制Global.asax 里也能写Application_BeginRequest之类的事件ASP.NET Core 全部统一为中间件Middleware顺序很重要执行顺序由app.UseXxx()的调用顺序决定。这不是一个简单的“替换”操作而是一次架构层面的重建。举个例子老项目里可能有一段代码在每次请求进来时用 Session 查数据库、写审计日志。在新平台里你要把它写成自定义中间件类public class AuditMiddleware { private readonly RequestDelegate _next; public AuditMiddleware(RequestDelegate next) { _next next; } public async Task InvokeAsync(HttpContext context, AppDbContext db) { // 请求前逻辑记录开始时间、读取请求头、写日志 await _next(context); // 请求后逻辑记录状态码、耗时 } }记得把这中间件注册到Program.cs的管道中间。顺序讲究比如UseRouting()必须在MapControllerRoute()之前UseStaticFiles()必须在UseRouting()之前虽然静态文件不会进 endpoints但最佳实践是先静态后路由UseAuthentication()和UseAuthorization()必须在 mapping 之前。如果你把顺序搞反了你会看到一些妖魔鬼怪般的 bug登录状态读不出来、静态资源 404、路由错乱实际上都是管道顺序问题。配置系统这块老项目里的Web.config的appSettings和connectionStrings被新的appsettings.json 环境变量取代。我建议所有敏感配置密码、连接串一律走环境变量或 Secret Manager不要直接写进 appsettings.json 并提交到 Git。连接字串的格式从老的add nameMyDb connectionStringServer.;DatabaseMyDb;Integrated SecuritySSPI; /变成{ ConnectionStrings: { MyDb: Server.;DatabaseMyDb;Integrated Securitytrue;TrustServerCertificatetrue; } }注意Integrated SecuritySSPI和true的区别以及新 SQL Server 连接串里对加密和证书的默认策略调整否则会连不上这些细节在迁移时很容易踩。还有一处容易翻车老代码里到处都是ConfigurationManager.AppSettings[key]的读法换到新平台后ConfigurationManager不存在了需要改为在构造函数里注入IConfiguration然后_config[key]。这个替换看着简单但如果是几千处调用的老项目会让你查得手软。我在大项目迁移时会先全局搜索ConfigurationManager数量然后按模块分批改不追求一天内完成但每次提交只涉及清晰的一个模块。7. 那些避不开的痛点HttpContext、Session、认证与授权7.1 HttpContext 全面改造在旧版 ASP.NET 里HttpContext.Current是全局静态的你在任何地方都能直接取到当前请求的信息QueryString、Session、User。ASP.NET Core 彻底干掉了这个静态访问改成构造函数注入IHttpContextAccessor或者从本身就是依赖注入的 Controller 基类里直接访问HttpContext属性。这个变化对“什么都不能改、快速迁移”的项目来说是非常难受的。因为老项目里往往会有一个自定义的工具类里面写着public static string GetCurrentUserId() { return HttpContext.Current.User.Identity.Name; }新平台里这行代码直接编译失败。正确的姿势是public class CurrentUserService { private readonly IHttpContextAccessor _httpContextAccessor; public CurrentUserService(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor httpContextAccessor; } public string GetCurrentUserId() { return _httpContextAccessor.HttpContext?.User.Identity?.Name; } }然后在Program.cs里记得注册builder.Services.AddHttpContextAccessor()否则注入永远是 null。这个坑我见得太多了几乎每个老项目迁移都会撞一次。7.2 Session 和 Cookie 的行为差异Session 在 ASP.NET Core 里不再由 System.Web 管理而是通过中间件开启。你需要builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options { options.IdleTimeout TimeSpan.FromMinutes(20); }); app.UseSession();顺序上UseSession()必须放在UseRouting()之后、MapControllers之前。另外Session 的 key 默认不再使用原先的ASP.NET_SessionId而是Cookie携带AspNetCore.Session标识如果客户端有脚本依赖这个 cookie 名称要对齐或者直接告诉前端改一下。Cookie 的行为变化也很大。老项目里如果做了“记住我”之类的登录持久化生成的 cookie 是ASPXAUTH或者旧版 FormsAuthentication 生成的形式新框架里使用ApplicationCookie前缀默认命名AuthCookie或你自定义的Cookie.Name。为了平滑迁移你需要在配置里把 cookie 名称显式指定为兼容模式并在用户首次访问时让新旧 cookie 并存过渡。很多人遇到“登录后跳转正常但刷新又要求登录”的诡异问题多半就是 cookie 名称或验证方式不一致导致的。7.3 认证与授权从 FormsAuthentication 到 Cookie 认证如果老项目用的是FormsAuthentication.SetAuthCookie就是那个经典的AccountController代码那么新平台已经彻底移除了这个 API你要替换成 ASP.NET Core 的 Cookie 认证中间件。配置大概是这样builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(options { options.LoginPath /Account/Login; options.LogoutPath /Account/Logout; options.Cookie.Name MyApp.Auth; options.ExpireTimeSpan TimeSpan.FromDays(7); });然后Controller里FormsAuthentication.SetAuthCookie对应要改成HttpContext.SignInAsync(principal)注销对应HttpContext.SignOutAsync()。这块改动并不大但需要注意 Claims 的方式。老代码里通常直接用用户名区分角色比如Roles.AddUserToRole新框架习惯用 Claims 的ClaimTypes.Role来做角色标记而且角色判断时推荐使用[Authorize(Roles Admin)]特性而不是手动if (User.IsInRole) else。另外一个非常重要但常被忽视的点老项目里的 URL 授权规则web.config中location加authorization在 ASP.NET Core 里直接失效。你必须在代码里通过[Authorize]属性、全局授权策略或者中间件来重建授权映射。否则你以为某些页面是受保护的实际上面向所有人开放了。这是高风险的“静默安全问题”上线前必须逐个 URL 验证权限。7.4 关于async的那个历史遗留问题老 MVC 项目里最常见的问题之一是为了省事把所有 Action 写成同步的但底层 EF6 查数据库都是同步阻塞。到了 .NET 8你几乎有义务把所有涉及 I/O 的操作改成 async原因很简单现代应用对高并发的要求更高线程池资源就是你的核心竞争力。如果你继续用.Result、.Wait()或者同步 EF 调用在 ASP.NET Core 下会导致线程池饥饿高流量时整体吞吐急剧下降甚至出现请求排队超时。这个问题在 .NET Framework 时代不明显是因为每个请求绑一个线程的执行模型有天然的“并发隔离”虽然浪费但现在不是这样了。我在迁移中遇到过一个经典案例一个报表页面里面连续做了五六个数据库查询全部是同步调用在老平台上跑 8 秒没人抱怨迁到 .NET 8 后一上线流量稍微大一点就卡到 20 秒以上。最后找出原因就是同步 IO 导致线程池耗尽。改成await之后同样数据量下响应时间回到 1 秒左右。所以不必把 async 改造当成“顺手优化”而是新平台固有的行为模式。8. 第三方库和工具类代码的迁移取舍与替代现在聊聊一些你可能没想过的边角料但往往会耗掉你两三天时间。AutoMapper 的配置迁移。老项目里通常有AutoMapperConfiguration.RegisterMaps()这样的静态配置方法新版本里你更推荐写在 Profile 类或模块化配置里。不过核心映射关系基本不用改主要是配置初始化的位置变了。一个更容易踩的问题是AutoMapper 新版本基于 .NET Standard 2.0但与 System.Text.Json 的交互可能存在大小写不一致的问题注意配置MapperConfiguration时设置 PropertyNamingPolicy。Newtonsoft.Json 与 System.Text.Json 的分歧。上边也提过ActionResult 默认序列化从 Newtonsoft 换成了 System.Text.Json这导致 JSON 输出里的属性命名策略不同。举个例子老代码里如果因为全局配置而输出 camelCase首字母小写新框架默认是 PascalCase首字母大写前端 JavaScript 端口如果写死res.data.UserName而不是res.data.username就会瞬间爆炸。如果你希望尽量保持行为一致在 Program.cs 里配一下builder.Services.AddControllersWithViews() .AddJsonOptions(options { options.JsonSerializerOptions.PropertyNamingPolicy System.Text.Json.JsonNamingPolicy.CamelCase; });或者干脆切回 Newtonsoft如果你有一堆依赖 Newtonsoft 特性的自定义格式化器builder.Services.AddControllers().AddNewtonsoftJson();日志框架迁移。如果老项目用 NLog 或 Serilog新框架自带的 ILogger 足够覆盖大部分需求但如果需要结构化日志、写到特定文件等仍建议花 10 分钟配置 Serilog。自己写自定义 Appender 的年代结束了换一个设计更现代的日志系统能少受很多苦。HtmlHelper、AjaxHelper 这类辅助方法。老 MVC 里的Ajax.BeginForm、Ajax.ActionLink都依赖 unobtrusive Ajax 或者 Microsoft jQuery 的.ajax库这些在 ASP.NET Core MVC 里不存在也不再被推荐。你需要把相关前端交互用标准的 fetch 或 jQuery.ajax 自己重建。这个工作量往往被严重低估因为写起来容易但设计上的交互模式变了以前是服务端返回 HTML 片段前端带X-Requested-With: XMLHttpRequest头去拿部分视图现在常见的做法是服务端返回 JSON前端渲染。如果你要保留原来的 HTML 片段方式可以返回PartialView()再配合 fetch 注入但把“视图渲染逻辑”和“数据加载逻辑”混在一起长远看对维护不友好。9. 迁移后的系统验证坑已填平怎么确认能上线迁移不是“编译通过 本地能跑”就完事的上线前的验证我有一条铁律找一份生产环境导出的匿名化数据副本在暂存环境跑完整业务回归。第一步功能验证。拿一张业务流程图把核心用户旅程走一遍注册、登录、浏览列表、搜索、下单或你对应的业务动作、查看详情、退出登录。每一步记录状态码、响应时间、是否有 JS 报错。这里我能给你的建议是不要只在浏览器里手动点写一套基础的冒烟测试或至少用 Postman 把主要 HTTP 接口全量跑一遍对比迁移前后的响应结构字段名、顺序、类型。第二步性能基线对比。在老系统和迁移后的新系统部署同样的环境或尽量接近用相同并发、相同数据量压测几个核心 API。主要看 P95 和 P99 延迟、吞吐量每秒请求数、以及内存占用变化。我通常会用dotnet-counters和dotnet-trace来观察 GC 频率和线程池状态如果发现明显飙升优先排查是不是有同步 IO、未释放的 DbContext 或过度使用Task.Run。第三步安全回归。老项目一般跑了很多年各种漏洞修修补补。迁移后要确认[Authorize]生效、CSRF 令牌正常工作、输入验证没有被新框架默认配置破坏。特别注意ASP.NET Core 默认对[ApiController]有自动模型验证但这只作用于控制器标注了该特性的场景。老 MVC 项目如果没标[ValidateAntiForgeryToken]的安全习惯迁移时往往不会自动补齐要逐页审查。第四步路由回归。老项目里 SEO 友好、带可选参数、带 catch-all 的路由规则很多在迁移后需要一条条验证。尤其是带*通配符的路由在老版本里能匹配多段路径新框架的 route pattern 通配语法不同用{**path}写错了会 404 一片。建议做一个路由清单表格把旧路由模板、实际示例 URL、期望 Controller/Action、迁移后请求结果全部列出来。最后是部署验证。不管你是直接部署到 Windows Server IIS、还是已经上 Docker都要验证静态文件缓存头、WebSocket如果有、HTTPS 重定向这些行为是否一致。IIS 下老项目的web.config里可能配置了各种 HTTP 模块迁移后要删掉否则新平台会报“无法加载模块”的错误。Docker 镜像里要注意 .NET 运行时版本、时区、环境变量这些都是经典问题。10. 上线后我回头看的十条经验我前后给不同的项目做过迁移辅导也翻过不少老一辈开发的代码。在这篇文章的最后我想直接列出十条我最想让你记住的经验每一条都是我和我带的团队实际踩出来的不是从文档里抄出来的理论老项目的“行为”并不都是对的。迁移过程中如果看到一个 SQL 慢查询、一个反模式写法别为了“不改功能”而无脑照搬。新平台给机会重写的地方顺手修掉比后续再折腾划算。大量使用HttpContext.Current的项目不要试图一次性改完。先全局替换成注入模式把静态访问隔离在一个专门的服务里再慢慢裁剪。一步到位会很痛苦。Controller 里的业务逻辑永远不要在迁移时继续堆积。新平台默认支持的最小 API、MediatR 之类的中介模式是更好的归属但你不需要在迁移阶段引入别贪心。EF Core 的迁移工具链非常依赖dotnet ef提前在 CI 管道里安装好。否则迁移脚本不一致数据库版本管理会乱套。Session 和 TempData 的默认实现会让你悄悄踩坑。如果老项目在分布式部署Session 用了 StateServer 或者 Redis记得在新平台选对 IDistributedCache 实现并且配置同一套 Redis否则多实例部署后用户登录状态一会儿在、一会儿丢。不要把 secrets 放进 appsettings.json。环境变量或 Secret Manager 才是正道。这是很多从 Web.config 时代过来的人改不掉的坏习惯。配置 Logging 的数据库连接字符串是小事但注意日志输出路径和格式。迁移后如果你硬编码了/logs/app.log在 Docker 里会因为目录无权限直接启动失败。Static Files 的处理方式不同了。旧项目在 web.config 里设置的自定义静态文件 MIME 类型迁移后要重新配否则某些特殊文件类型比如.json、.woff2出现 404 或错误 Content-Type。API 层面的 JSON 变化要提前和前端沟通。前端很可能一直依赖旧的 JSON 序列化细节比如时间为2025-01-01T00:00:00而不是老 ASP.NET 站点常用的/Date(233...)/格式。这个要是通宵上线才发现火葬场。迁移完成不是终点把项目的 runtime 版本持续升级机制建立起来。.NET 8 之后有 .NET 9、10LTS 之间升级的成本远小于从 Framework 跨到 Core 的成本。长期把项目停留在老版本下一次被迫升级时又会是今天这个痛苦的局面。我在迁移一个内部管理系统时把这篇文章里的步骤完完整整走了一遍。开头崩溃了三天等到把项目文件格式、Startup、认证、EF Core 四大硬骨头啃下来后面的进度就顺多了。如果你正准备动工建议今晚先把“迁移难度评估表”拉出来然后在新分支里创建一个空的 .NET 8 项目把一个最简单的 Controller 跑通。这段小小的成功经验会给你接下来几周注入足够的信心。祝早日上线。