在很多公司,有一场对话大约每年上演一次。有人指出,大家都依赖的那套内部应用是很久以前写的,用的是如今已无人再提的框架,写它的人也大多不在公司了。另一个人问:换掉它要多少钱。报回来的数字很大。所有人都同意这件事很重要。然后什么也没发生。十二个月后,同一场对话从头再来一遍。
它之所以卡住,是因为问题问错了。「换掉它要多少钱」没有有用的答案,因为这类应用大多数并不该被替换。它们该被迁移:分批地、按顺序地,并且有一部分要原封不动地留在那里。
「它老了」不是理由
年头不是缺陷。一套正确运行了十二年的应用,已经证明了新应用尚未证明的东西。如果它在赚钱、用它的人对它了如指掌、而且没有哪里正在着火,那么框架版本只是一个事实,而不是一个问题。
真正逼你做决定的情形要窄得多,也值得说白,因为如果下面这些都不成立,你可能正要花一笔没有回报的钱:
- 你招不到人。这是最常见、也最少被讨论的真实触发点。当愿意碰这套代码的人少到一定程度,风险就不再是技术层面的了。
- 它依赖的某样东西已停止支持。不是应用本身,而是它底下的:操作系统、框架、数据库版本、某个支付集成,或者某个已知有漏洞、却没有修复版本的库。
- 业务需要架构给不了的东西。移动端访问、给合作伙伴的 API、单点登录,或者某个默认采用现代认证流程的集成。
- 它挡住了别的事。撤出机房、一项合规要求、一次并购,或者一次改变了底层契约的 ERP 升级。
- 做一点小改动的代价已经离谱。当改两行代码要花三周,只因为没人能安全地测试它,那就是一项真实且可度量的税,而且会不断累积。
只要其中一条成立,这个项目就有商业理由。如果一条都不成立,诚实的建议是:把安全与依赖的活儿做掉,把这东西的行为写下来,一年后再看。我们宁愿这么说,也不愿卖一次没人需要的重写。
几条路径,以及各自真实的代价
Web Forms
这条最直白。ASP.NET Web Forms 只能跑在 .NET Framework 上。它从未被带向新平台,也永远不会。既没有兼容层,也没有哪种自动转换能产出一套可用的现代应用,因为它的整个模型,连同页面生命周期、ViewState 和服务器控件,在另一侧根本没有对应物。
所以界面层就是一次重写。但它背后的一切并不是。在我们看过的多数 Web Forms 应用里,真正的价值有很大一部分落在业务逻辑与数据访问类里,它们只是普通的 C#,迁移到当前 .NET 的阻力比任何人预想的都小得多。现实的做法是:保留逻辑、把它移植过去,再按各个界面需要多强的交互性,用 Razor Pages、MVC 或 Blazor 重建页面。
陷阱在于:因为前端要重写,就把整套应用都按重写来估。九个月的项目被报成接近三个月的活儿,通常就是这么来的。
ASMX 与 WCF 服务
ASMX 与 Web Forms 处境相同:只有 .NET Framework,没有向前的路。WCF 则更微妙一些,因为有 CoreWCF;如果你确实需要 WS-* 那一套、SOAP 契约或那些传输方式,它能把服务近乎原样地带过去。
通常你并不需要。多数内部的 ASMX 与 WCF 端点,做的事情用一个基于 HTTP 的普通 JSON API 会更简单,而调用方本来就是你自己的、可以一并更新。常见的落点是 ASP.NET Core 的 minimal API 或控制器,并在迁移途中把契约梳理干净,而不是原样复刻。仅仅因为 2011 年它就在那儿,就让一个 SOAP 信封继续活着,这种决定值得有意识地做,而不是默认继续。
如果调用方你改不了,因为另一头是合作伙伴或某台设备,那就让新 API 并行运行,并保留一个很薄的兼容端点。这比听上去便宜得多,而且能把你的时间表和他们的解耦开。
WinForms,以及大家最常做错的那次迁移
WinForms 没有死,而这一处纠正最省钱。WinForms 和 WPF 都能跑在当前的 .NET 上(Windows 平台)。一套 WinForms 业务线应用完全可以从 .NET Framework 迁到 .NET 10,并且继续是 WinForms。你拿到现代运行时、当前的依赖、更好的性能和一个受支持的平台,却一个界面都不用重写。
这一点之所以要紧,是因为面对「我们有一套老的 WinForms 应用」,如今条件反射式的回答越来越是「用 MAUI 重写」,而对很多应用来说这根本就是选错了工具。MAUI 面向的是跨平台:用一套代码覆盖 iOS、Android、Windows 和 macOS。如果你的应用就跑在自家楼里的 Windows 桌面上、而且今后也一直如此,那么 MAUI 什么也没给你带来,却让你付出一整套界面重写的代价。
当需求确实改变了形态时,MAUI 才是正确答案:你的外勤技术人员需要在手机上用它;你的销售团队需要在 iPad 上用它;你希望桌面和移动共用一套代码,而不是养两个团队。这些都是真实的理由;一旦成立,这活儿就值得认真做。我们把那条路径的细节单独写过一篇:Xamarin 到 .NET MAUI 迁移指南,其中很大一部分同样适用于从 WinForms 出发的情形。
行得通的顺序是:先迁到当前的 .NET,仍然保持 WinForms;再把「要不要去 MAUI」当作之后一个独立的决定,按它自身的价值来判断。两件事同时做,正是这类项目最后说不清到底是什么坏了的原因。
没有人会报价的那个选项
让它待着。把底下的技术栈打好补丁,修掉那些有已知漏洞的依赖,把应用做了什么写下来,再给它配一份维护安排。对于一套能正常工作、也没有挡住任何事情的应用,这往往是可选项里回报最高的一个;而它也正是卖重建的人永远不会向你提起的那个。
AI 在哪里真能加速,在哪里不能
迁移在很大程度上是一个跨越已知边界的翻译问题,而这恰恰是当前 AI 工具擅长的事。有纪律地使用,它能实质性地改变这件事的经济账;当成魔法棒使用,它会以任何人工评审都跟不上的速度,产出自信、看起来合理、实际错误的代码。
它帮得上的地方
- 读完代码,并告诉你里面到底有什么。每一个界面、每一个存储过程、每一条业务逻辑分支,都被整理成一份结构化的竣工记录,说明这套应用实际做什么、为什么这么做。对于一套十年来没人完整理解过的代码,这是价值最高的用法,没有之一;而它在人力速度下又足够枯燥,以至于从来不会真的被做掉。
- 机械性的翻译。Web Forms 的 code-behind 转成控制器加视图模型;ASMX 方法转成一个 minimal API 端点;旧的数据访问换成当下的写法。这些都是形态一致、重复度高的变换,而大批量下的一致性正是它的强项。
- 补上你从来没有过的测试。旧应用在被关停之前,一直是它自身行为的可运行规格说明。趁它还在跑的时候把这些行为固化成测试,你就有了检验新版本的基准。仅这一项实践,就把顺利的迁移和糟糕的迁移区分开来;而在它变便宜之前,几乎没人这么做。
- 依赖与漏洞的分诊。弄清四百个包里哪些真正要紧、动了它们会坏掉什么、以及该按什么顺序动。
它帮不上的地方
- 决定该保留什么。你的应用所做的事情里,有些是刻意设计的业务规则,有些是为绕开某个早已不存在的限制而留下的变通,还有些是多年前大家默默适应了的缺陷。要分清它们,必须与使用它的人交谈。没有任何模型知道哪个是哪个;而一个被自信地「修好」的所谓缺陷,实际上却是一条规则,这就是你损失掉一个月的方式。
- 架构。这该是一个服务还是四个、那个队列是否必要、当某个依赖变慢时的失效模式是什么。这些都是判断,而后果要到第三年才显现。
- 任何你没有验证的东西。能编译的生成代码,不等于正确的生成代码。评审的工作量是实打实的,不会消失;一个跳过它的团队,会把一次迁移变成一次事故。
诚实的说法是:AI 并不能免掉这个项目。它免掉的是绝大部分考古工作、绝大部分机械翻译,以及「没有测试套件」这个借口。这已经是成本中的很大一块和风险中的绝大部分,但这与「AI 帮你重写应用」是非常不同的主张。
所谓面向未来,其实是指下一次迁移
「面向未来」这个词通常什么都没说。把它落到实处,它只意味着一件事:让下一次这样的迁移更便宜。框架还会继续变。目标不是选一套能用到永远的技术栈,因为没有哪套能;目标是让自己处在这样一个位置:再迁一次是一个项目,而不是一场危机。
真正能做到这一点的,按价值大致排序:
- 一套描述行为的测试。它是未来某次迁移是否安全的最大决定因素。如果这篇文章你只带走一条,就带走这条。
- 不住在界面里的业务逻辑。Web Forms 迁移之所以痛,正是因为十年的规则最后都堆进了 code-behind 文件。住在自己那一层里的逻辑,迁到下一个平台几乎是免费的。
- 写下来的需求。不是关于代码的文档,那种会腐烂;而是关于决策及其缘由的记录,那种不会。
- 无趣而当前的依赖。库更少,选择标准是「有人维护」而不是「设计巧妙」,并且按例行节奏更新,而不是等到火烧眉毛。
- 把配置与密钥放到应用之外。做起来很便宜,却能消掉整整一类迁移时的痛苦。
- 一个随时能跑的部署流程。如果发布本身很难,那么发布之后的一切也都会很难。
一套老化的技术栈实际暴露了什么
老系统的安全问题总是被抽象地讨论,而这让它很容易被一再推迟。具体到我们做过审计的那些应用上,反复出现的是同一份很短的清单:
- 已停止支持的框架或运行时。.NET Framework 4.6.x 已于 2022 年 4 月失去支持,而大量应用仍停在上面。升到 4.8 通常并不复杂,而且即便你永远不离开 .NET Framework,这也是一次真实的改进。
- 已停止支持的操作系统。Windows Server 2012 R2 已于 2023 年 10 月退出支持,而它仍在比任何人愿意承认的更多地方跑着生产。
- 从最初部署沿用至今的 TLS 与加密套件配置。旧协议版本仍然开着,既通不过现代扫描,也越来越多地破坏与支付服务商和合作伙伴的集成。
- 已公布漏洞且已有修复版本的依赖。真实发现里毫不光鲜的大多数。并不高深,只是从来没人去做。
- 早于当下实践的认证方式。自制的会话处理、用 2013 年还算合适的算法哈希的密码、没有任何走向多因素的路径。
- 放在配置文件里的密钥。躺在源代码管理中,任何曾经拉过一次代码的人都能读到。
这些几乎没有一样需要那次重写。它们需要的是有人去看,然后按优先级把活儿做掉。这也正是下一节的理由。
评审、审计,以及这场对话里比较难开口的那个版本
有时候真正有用的合作根本不是一次迁移,而是对代码以及围绕它的团队做一次外部评估,并把结论交给那个必须做决定、却一直听到相互矛盾答案的人。
它产出什么:
- 一份竣工画像。这套应用实际是什么、依赖什么、风险集中在哪里、哪些是死代码、哪些只是看起来像死代码。
- 一份安全与依赖的现状。按「是否真的可达」排序的发现,而不是一份有四百条「严重」、没人会读的扫描器导出。
- 一组估好价的选项。留下并维护、分批迁移、重建。附上真实的数字和推导过程,其中也包括「做得最少」的那个方案。
- 对交付能力的诚实判断。这个团队能否执行这套计划、缺的是什么、以及瓶颈到底在人、在流程还是在工具。这一部分是客户私下才会问的,而且往往才是真正的问题。
如果一个内部团队本来就做得不错、并不需要我们,我们会照实说;因为一份永远得出「请雇用评审方」结论的评审,对读它的人毫无价值。
一套行之有效的顺序
- 先趁旧系统还在运行时,把竣工状态记录下来。要早于任何新平台上的工作。它一关,这扇窗就合上了;而先做这一步,后面的每一步都更便宜。
- 立刻把安全与依赖的活儿做掉。它独立于其他任何决定,相对便宜,而且是那个由别人来定截止日期的部分。
- 把行为固化成针对旧系统的测试,好让新系统有一个可对照的基准。
- 先迁运行时,再动架构。先到当前的 .NET,形态不变,WinForms 仍是 WinForms。之后再单独就界面做决定,按它自身的价值判断。
- 按能上线的切片来迁移。一个服务、一组界面、一个集成。任何六个月内上不了线的东西,也就六个月内改不动。
- 让新旧并行,直到一次真实的切换。在关掉旧路径之前,先让真实用户走在新路径上。
结论
多数老旧 .NET 应用的实际状况,比围绕它们的那些讨论所暗示的要好。框架不再时髦,不是一个商业问题。招不到人、打不了补丁、改不安全,才是商业问题;而它们各有不同的解法和不同的价签。
先拿到那份诚实的清单。无论如何都把安全的活儿做掉。先迁运行时,再动架构。把 AI 用在考古和翻译上,而不是用在判断上。并且,对任何在读你的代码之前就建议全面重写的人,保持怀疑。
你的风险清单上正躺着这样一套系统吗?
我们基于 .NET 10 与 .NET MAUI 做开发,也在别人写的、且没人完全理解的应用里待过很久。如果你想要一个关于「你的选项究竟要花多少代价」的直接判断,无论那是一次迁移、一轮安全与依赖梳理,还是一次对代码与团队的外部评审,都欢迎告诉我们你在运行什么。如果结论是「别动它」,我们也会照实说;那是一个真实的结论,而不是客套。你也可以进一步了解我们如何做定制软件与跨平台移动开发。