微软:重新构想 .NET 的构建与发布方式(再次)


目录

在撰写完关于.NET构建与交付流程的上一篇文章后,我曾谨慎乐观地认为无需再写类似内容——至少不必再探讨构建与交付机制。这个问题已尘埃落定。.NET做到了!我们在分布式仓库开发与快速组装产品交付能力之间找到了平衡点。恭喜各位,现在基础设施团队可以专注于其他领域了——安全、跨公司标准化、新产品功能构建支持。全是些美差。

……一年半后……

我们开始盘算每月要构建3-4个主版本,期间还要发布十几个.NET SDK版本,同时保持工程系统更新。更棘手的是:有个紧急修复需要赶进下周发布版,能否今天提交代码让团队今晚验证?这应该不难吧?我还有个跨栈功能想做原型测试…该怎么构建?

得到的答案大多令人沮丧:

“成本会很高,而且会随着时间推移越来越糟。”

“我认为没时间做那个修复,构建耗时只能估算,但至少需要36小时才能移交验证。“ 可能更久?

虽然能维持这么大的基础设施运行,但更新维护成本会让我们慢慢被淹没。

全栈环境对你有多重要?搭建起来需要时间。

这些绝非我们期望给出的答案。于是我们重回设计阶段,寻求解决方案。

元素周期表

本文将介绍统一构建项目:这是.NET为解决上述问题而推出的方案,通过将产品构建迁移至“虚拟单体”仓库,将构建流程整合为一系列“垂直构建”,同时允许贡献者在单体之外开展工作。我将简要讲述.NET发展历程中的产品构建演进史,重点阐述将分布式构建模型应用于单一产品的经验教训——特别是其在开销和复杂性方面的弊端。最后深入剖析统一构建及其核心技术Linux发行版源构建,探讨新型构建方法及其成效。

我们如何走到今天?这并非我理想中的构建基础设施§

.NET诞生于2015-2016年间.NET Framework和Silverlight的闭源架构。随着组件逐步准备就绪供外部使用,我们按当时的惯例将其拆分为多个仓库,并逐步开源。CoreCLR代表基础运行时,CoreFX提供库文件,Core-Setup负责打包与安装。随后ASP.NET Core与EntityFramework Core相继问世,并推出带命令行工具的SDK。经过数次重大版本更新,产品通过共享框架实现全面革新,WindowsDesktop框架也加入其中。仓库数量持续增加,体系日益复杂。

关键在于理解:.NET虽由相互依存的独立仓库开发,但必须在相对短时间内整合交付。从理论上讲,该产品的“图”结构与任何开源生态系统极为相似:仓库生成软件组件,将其发布至公共注册表,下游消费者引入新组件依赖,并发布自身更新。这种生产者-消费者模型通过一系列拉取->构建->发布操作,使变更在“全局”依赖图中层层传递。该模型高度分布且高效,但在时间维度上未必高效。它赋予软件供应商和仓库所有者对流程与日程的高度自主权。然而,若将此方法论应用于.NET这类通过独立但相互依赖的仓库表示组件的产品,则存在重大缺陷。

我们称之为“分布式产品构建方法论”。为理解其应用难度,我们以安全版本发布流程为例说明。

示例:安全维护§

假设需要发布安全补丁:在.NET运行时库中发现安全漏洞。由于.NET源自.NET Framework,假设该漏洞同样存在于.NET Framework 4.7.2版本。此时必须确保.NET的安全更新与.NET Framework更新同步发布,否则一方将使另一方暴露于零日攻击风险。而.NET存在多条由微软管理的发布路径: 包括Microsoft Update、我们的CDN、Linux及容器包注册库、nuget.org、Visual Studio、Azure Marketplace等渠道。这使得时间线受到限制,我们需要具备可预测性。

.NET 的开发架构与典型开源生态系统高度相似。虽然存在大量跨团队协作,但 .NET 运行时、.NET SDK、ASP.NET Core 和 WindowsDesktop 共享框架由不同团队开发,有时甚至像独立产品般运作。其中 .NET 运行时构成产品基础,ASP.NET Core 和 WindowsDesktop 则构建于其上。大量开发工具(C#、F#、MSBuild)构建于.NET运行时及辅助库之上。SDK整合构建了命令行界面(CLI)、任务、目标及工具集。共享框架与工具内容大多通过内置方式重新分发。

为构建并发布此安全补丁,需要协调参与.NET整体产品开发的多个团队。我们需要.NET图谱(见下文)的底层组件先构建其资产,再向下游消费者传递。下游团队需接收更新、构建并继续向下传递。此过程将持续进行直至产品达到“一致性”状态; 即图中不再引入新变更,且所有组件版本达成统一共识。一致性确保变更后的组件能被所有分发该组件或相关信息的节点同步更新。随后我们将进行验证,从所有未发布组件的闭包中提取可交付资产,最终一次性向全球发布。

这涉及众多动态组件,需在短时间内协同运作。

分布式生态系统的优缺点§

需要强调的是,这种分布式生态系统开发模式确实具有诸多优势:

  • 分层化——仓库边界往往能促进分层设计,降低产品间的耦合度。在主要版本开发周期中,即使变更在图谱中快速且不均衡地流动,堆栈中的各个组件通常仍能保持基本兼容性。
  • 社区建设——仓库边界有助于培育专注高效的开发社群。例如WPF与Winforms社区往往保持独立。小型仓库通常也更易于参与。
  • 增量性——分布式开发通常支持增量变更。例如,我们可以对 System.CommandLine 接口进行破坏性变更,随后逐步让消费者接纳这些变更。这种方式并非总能奏效(例如当 SDK 试图为所有工具提供单一版本的 System.Text.Json 时,但并非所有消费者都认同该接口设计。砰?!),但总体可靠性较高。
  • 紧凑的内部循环——规模精简、聚焦明确的仓库往往能提供更高效的内部开发体验。即便像git clonegit pull这样基础的操作,在小型仓库中也更迅速。仓库边界能营造(或许是虚幻的)安全感:开发者只需专注于可见范围内的代码与测试。
  • 异步开发——增量特性有助于实现更高效的异步开发。当我的组件流向三个位于不同时区的下游消费者时,这些团队可在各自时间推进组件开发,无需协调同步。
  • 低成本分片/增量构建——分布式开发允许对依赖图边缘、变更频率低的组件进行“优化”性构建。例如,构建静态测试资源的叶节点无需随SDK每次变更重建,上次构建的资源完全可用。

但若仔细推敲,当需要在短时间内完成图谱中大量组件变更的软件构建与发布时,分布式模型的诸多优势反而会成为显著弱点。大规模图谱变更往往缓慢且不可预测。但为何如此?这种模型是否存在根本缺陷?并非如此。在典型的开源生态系统(如NuGet或NodeJS包生态)中,这些问题通常不会显现。这类生态系统并不追求速度或可预测性,而是重视每个节点的自主性。每个节点只需关注自身需要 生产消费 的内容,以及满足这些需求所需的变更。然而当我们试图将分布式模型应用于快速交付软件时,往往会陷入困境——因为这加剧了两种关键概念的蔓延,我称之为“产品构建复杂度”与“产品构建开销”。二者共同作用导致效率下降与可预测性降低。

产品构建复杂度

在产品构建语境中,“复杂度”指的是变更从开发者机器流转至最终交付给客户所需经历的步骤总量——涵盖所有必要的交付方式。我承认这个定义相当抽象。“步骤”的具体内涵取决于观察的粒度级别。现阶段我们聚焦于概念层面的产品构建步骤,如下图所示:

图0:微软:重新构想 .NET 的构建与发布方式(再次) 一个简单的多仓库产品构建工作流。MyLibrary和MyApp分别基于独立代码库构建,MyApp部署至两个客户终端

.NET最初拥有相对简单的产品依赖关系图及其配套管理工具。随着规模扩大,新仓库不断加入图结构,构建产品需要更复杂的依赖流。图结构日益复杂。我们为此发明了新工具(Maestro,我们的依赖流系统)来管理它。此时添加新依赖变得前所未有的容易。开发者或团队若想为产品添加新功能,通常只需创建新仓库,构建并配置输入输出即可。他们只需了解该组件在更大产品构建图中的某个子集中的定位,就能添加新节点。然而.NET不会独立发布每个单元。产品必须达到“一致性”——即所有参与者就依赖项版本达成共识——才能发布。依赖项及其元数据需要重新分发,这要求你必须“遍历”所有边。 注:虽然无需每次迭代都更新图中的所有组件,但相当一部分组件会因修复或依赖流变化而随每次发布更新。 随后将各节点的输出整合后即可交付。

更复杂的图结构存在显著弊端:

  • 边与节点越多,达成一致性所需时间往往越长。
  • 团队更容易出错。协调点增多,工作流中人类可影响结果的环节也随之增加。工具虽能提供帮助,但终究存在局限。
  • 复杂性还会导致构建环境和需求出现差异。当团队以不同速度迁移和升级时,很难让所有人保持流程一致。完整复现所有环境成本高昂,且随着基础设施“老化”,这种成本往往随时间推移而增加。

图1:微软:重新构想 .NET 的构建与发布方式(再次) 此为.NET产品构建图中微小但关键的子区域(约.NET Core 3.1版本)。Arcade提供共享构建基础设施(虚线),实线则表示组件依赖关系。变更需穿越多个代码库层层传递,最终才抵达SDK和安装程序。

产品构建开销

我们将开销定义为“ 未直接用于生成可交付客户的构建成果所耗费的时间 ”。如同复杂度评估,其细化程度可根据需求灵活调整。以下通过两个简例,结合.NET早期构建版本的开销数据进行说明。

简化版多仓库产品构建流程示意图:

图2:微软:重新构想 .NET 的构建与发布方式(再次) 图示展示简单多仓库产品构建流程中的开销环节。虚线节点代表开销环节。

在上述图示中,开销节点(虚线节点)并未直接参与D仓库中包的生成。依赖流服务创建PR所耗费的时间即为开销。等待开发人员注意到并审核PR是开销。等待包推送审批是开销。这并非说这些步骤不必要,而是指这些环节中我们并未为客户主动创造产出。

那么构建环节呢?若聚焦单个仓库的构建流程,常能发现大量开销。以这个极简构建流程为例:

图3:微软:重新构想 .NET 的构建与发布方式(再次) 图示展示简单管道中的开销。点状轮廓节点代表开销。需注意,其中存在多个未实际生产或向客户传输数据的步骤。这些步骤虽可能必要,但仍属于开销范畴。

系统开销存在几种有趣的衡量方式。我们可以将其作为总耗时百分比进行测量。根据步骤分类统计其耗时,再将总开销除以总耗时。这能有效衡量整体资源效率。但从实际耗时角度看,总开销数据意义有限。要理解开销对端到端时间的影响,需在产品构建图中找出耗时最长的路径,计算该路径中所有贡献步骤的总开销占路径总耗时的比例。

为了解.NET单次构建中的开销表现,我们以运行时8.0版本构建为例。该数据通过定制工具生成,该工具能基于分类步骤的模式集评估Azure DevOps构建过程。

指标 时间 占总构建时间百分比
所有步骤(含队列) 2天02小时18分10.9秒 100%
开销(含队列) 19小时23分22.9秒 38.5%
开销(无队列) 12:33:36.6 25.0%
队列处理 06:49:46.3 13.6%
实际工作 1天06:42:10.7 61.0%
未知 00:12:37.3 0.4%
———– ———- —–
最长路径时间 05:40:05.2 N/A
平均路径时间 04:03:11.3 N/A

以下是该构建中三个最长的路径:

路径 总耗时 开销时间(含队列) 队列时间 工作时间 未知时间
(阶段) 构建->Mono浏览器AOT偏移->windows-x64发布CrossAOT_Mono crossaot->构建工作负载->(阶段) 发布准备->准备签名工件->发布资源 05:40:05.2 02:46:49.8 (49.1%) 00:40:29.8 (11.9%) 02:51:39.0 (50.5%) 00:01:36.3 (0.5%)
(阶段) 构建->windows-arm64 release CoreCLR ->构建工作负载->(阶段) 准备发布->准备签名工件->发布资源 05:37:32.0 02:28:58.1 (44.1%) 00:31:32.2 (9.3%) 03:07:05.6 (55.4%) 00:01:28.2 (0.4%)
(阶段) 构建->Mono android AOT偏移量->windows-x64 release CrossAOT_Mono crossaot->构建工作负载->(阶段) 准备发布->准备签名工件->发布资源 05:37:00.9 02:47:19.1 (49.6%) 00:40:51.8 (12.1%) 02:48:05.0 (49.9%) 00:01:36.8 (0.5%)

开销 + 复杂度 = 时间

开销不可避免。每个产品构建流程都存在固有开销。但当我们增加产品构建流程的复杂度(尤其是图结构复杂度)时,开销往往开始主导整个流程。这种开销呈倍增效应——你可能不是只支付一次机器队列时间成本,而是在单次图路径遍历中支付十倍成本。分配完机器后,每次还需克隆仓库。这些步骤的效率扩展性往往更差,因为每个步骤都存在固定成本。例如:扫描10MB工件需10秒,准备扫描、整理和上传结果需1秒。连续执行该步骤10次所需时间,反而比一次性扫描完整100MB更长——110秒 vs 101秒。

更隐蔽的是,这类成本往往随时间推移悄然累积。开发者本地仓库构建通常很快,他们不会察觉整体CI系统的开销。但若将视角拉开,在管道任务中构建仓库虽同样迅速,却开始产生额外开销——仓库本身构建虽快,但外围步骤增加了负担。此时效率尚可维持。但若进一步放大视野,假设管道中新增了执行其他任务的作业——例如复用构建环节的产物、构建容器等——开销占比将显著攀升。再一次放大视角,当你审视该管道及其关联仓库在整体产品构建体系中的位置时: 此时需计入开发PR审批时间、依赖流系统处理时间、更多克隆操作、更多构建任务、更多合规检查——层层叠加的开销不断累积。

在分布式产品构建系统中,影响复杂度(进而影响开销)的决策往往在无法洞悉系统整体开销的层级上作出。新增节点在孤立状态下看似无碍,但置于整体环境中便会产生代价。

虽然从未有人绘制过.NET 8时期的复杂性图谱,无法展示单个组件构建在整体产品构建图中的复杂度,但仅看运行时构建的作业图谱就足以说明问题。下图每个气泡代表一台独立机器。

图4:微软:重新构想 .NET 的构建与发布方式(再次) .NET 8构建的复杂性示意图。每个节点代表一台独立机器。边表示依赖关系。

源构建中统一构建的根源§

.NET源代码构建是Linux发行版在隔离环境中基于单一统一源代码布局构建.NET的一种方式。微软约在.NET Core 1.1版本期间启动该项目。统一构建理念的灵感源于.NET源代码构建团队与微软发行版负责团队在走廊间的交流。我不会否认基础设施团队常怀着羡慕之情观察源代码构建环境中.NET产品的构建时长——仅需50分钟!这比官方CI构建中从零构建运行时仓库的时间还要短。当然,两者并非完全可比。毕竟源代码构建:

  • 仅构建单一平台
  • 不构建任何Windows专属资产(如WindowsDesktop共享框架)
  • 不构建.NET工作负载
  • 不进行安装程序打包
  • 默认不构建测试

这些限制都合理。但这些限制真能累积出数十小时的构建时间差吗?不太可能。更可能的是源代码构建方法本身具有低复杂度和低开销的特性。除了时间优势,还有其他明显益处:统一工具集、更便捷的跨栈开发,以及最关键的——对构建内容及其依赖关系的硬性保障。

回到那些走廊对话。源代码构建的明显优势,促使.NET团队成员不时提出探询式问题,大多是这样的: 那么…微软为何不采用这种方式构建发行版? 答案:因为难度太大。

为何困难?深入源代码构建领域§

微软在.NET 3.1时代开始将源代码构建打造成“真正的”生产化工具链。在此之前,每次.NET重大版本发布时,源代码构建分发都更像是临时拼凑的解决方案。持续维护工作难度过高,因此团队从春季新产品成型之初便着手行动,使新版.NET符合Linux发行版维护者的要求。要理解为何将微软的.NET发行版纳入统一构建项目如此困难,我们需追溯源代码构建项目最初难以实现持续构建状态的根源。

为使发行版合作伙伴能分发.NET,我们需构建一套在以下约束下生成.NET SDK的基础设施系统:

  • 单一实现!——每个组件仅限一种实现
  • 单一平台——仅针对单一平台构建(即发行版合作伙伴计划发布的平台)
  • 单一构建——仅在单台机器上构建。我们不能要求复杂的编排基础设施。

Linux发行版构建要求

Linux发行版在构建软件包时通常遵循更严格的规则且灵活性较低。构建过程通常离线完成(断开互联网连接),仅允许使用该构建系统中预先生成的构建产物作为输入。禁止使用已提交的二进制文件(尽管可在构建时消除)。仓库中的任何源代码都必须满足严格的许可要求。 有关.NET许可信息请参阅许可说明,发行版要求示例请参阅Fedora许可审批 从概念层面而言,Linux发行版合作伙伴期望能够追溯其发布的每个构建产物,确保其源自可合理编辑的源代码与流程。所有未来软件均应基于先前源代码构建产生的产物进行构建。 注:如您所料,此过程需包含初始化流程

单一构建——整合技术栈的仓库与编排框架

正如您之前所了解的,.NET 构建与许多产品类似,实际上是由 Azure DevOps 中各个组件的构建结果拼接而成,并通过依赖项更新进行整合。这意味着构建产品所需的信息和机制分散在多个仓库中(包括构建系统内的构建逻辑及相关脚本,以及由 Azure DevOps 处理的 YAML 文件),同时还涉及由我们的“Maestro”系统持有的依赖流信息(生产者-消费者信息)。这种模式无法满足我们的 Linux 发行版合作伙伴的需求。他们需要能够在不访问这些微软资源的情况下构建产品。且必须以符合其环境特性的方式实现。手动从构建图拼接产品显然不切实际。我们需要一个能封装这些信息的协调器。

源构建布局与协调器

该协调器取代了 Azure DevOps 和 Maestro 在 .NET 分布式构建中执行的任务,转而通过脱离互联网的单一源布局运行。您可在dotnet/dotnet查看现代化的更新版布局与编排器。

  • 单一源代码布局——包含构建产品所需全部组件副本的单一源代码布局。若存在子模块(通常为外部开源组件),则将其展开。源代码布局内容通过识别产品图谱中每个组件的注释依赖项来确定。该注释依赖项的sha值决定了布局中填充的内容。 注意:编译器和操作系统库等依赖项由构建环境提供。
  • 各组件构建方式及其依赖关系说明——单一源布局中的每个组件均提供一个基础项目,用于定义组件构建方式。此外还明确了组件级别的依赖关系。例如:必须先构建 .NET 运行时才能启动 ASP.NET Core。
    <ItemGroup>
    <RepositoryReference Include="arcade" />
    <RepositoryReference Include="runtime" />
    <RepositoryReference Include="xdt" />
    </ItemGroup>
    
  • 构建协调器逻辑 – 构建协调器逻辑 负责在图中每个构建就绪时(即所有依赖项已成功构建)启动该构建,并管理各组件的输入输出。组件构建完成后,编排器需识别输出结果并为下游组件构建准备输入。可将其视为本地Dependabot,通过计算声明的输入仓库与包级依赖信息(详见aspnetcore示例进行计算。有关.NET构建中依赖项跟踪机制的更多信息,请参阅我的先前博文
  • 合规性验证 – 由于 Linux 发行版合作伙伴构建环境相对严格,我们必须构建自动化机制来识别潜在问题。协调器可检测预构建二进制输入、“毒性”泄漏(即先前源代码构建的资产出现在当前构建输出中)及其他可能阻碍合作伙伴的风险。
  • 烟雾测试 – 多数测试逻辑仍保留在独立仓库中(详见后文),但架构中也包含烟雾测试

单一实现方案 – 预构建的纯净环境

使用微软原生构建的.NET框架难以满足这些要求,且源代码构建需要投入大量工作,其中既有显而易见的原因,也有隐性因素。实现离线构建需要预先准备可从源代码构建的明确输入项,这本身就是一项重大工程。当源代码构建团队开始探究其具体含义时,很快发现.NET产品构建中隐藏着大量复杂行为。诚然,优化数据等二进制输入显然被禁止使用,但某些基础组件(如.NET Framework和NETStandard目标包)同样无法从源代码构建。这些组件要么原本就未开源,要么多年未曾构建。更令人担忧的是,.NET 的图结构特性导致不一致现象普遍存在。其中部分不一致属于非预期状态(我们在产品构建过程中会努力消除),而另一些则属于预期甚至期望的状态。

示例:Microsoft.CodeAnalysis.CSharp

以C#编译器分析器为例,这些分析器构建于dotnet/roslyn仓库中。这些分析器会根据所需覆盖范围引用不同版本的Microsoft.CodeAnalysis.CSharp包,以确保发布的分析器能支持所有必需的Visual Studio和.NET SDK版本。它们采用最小必要版本策略,从而以可持续方式维护分析器,而非针对每种VS或SDK配置发布不同版本。

由于会引用多个版本的覆盖范围,构建过程中会恢复多个版本的Microsoft.CodeAnalysis.CSharp。这意味着在源代码构建过程中,我们需要在某个时间点构建所有这些版本的Microsoft.CodeAnalysis.CSharp。为此有两种方案:

  • 多版本源代码布局——将多个dotnet/roslyn副本放置于共享源代码布局中,每个副本对应原始生成时所引用的Microsoft.CodeAnalysis.CSharp版本。此方案不仅耗费大量构建时间,还容易产生连锁影响。若需构建三个版本的dotnet/roslyn,则必须确保这三个版本的传递性依赖项也存在于共享布局中。此类配置的维护复杂度会急剧攀升。这些是dotnet/roslyn源代码库的已发布版本,需长期维护其安全性和合规性,包括升级构建时依赖项、移除生命周期结束的基础设施等。
  • 要求可获取先前源代码构建的版本——这本质上是多版本源代码布局的变体,带有“缓存”特性。若发行版维护者需从头重建产品,或启动新的Linux发行版,可能需要重建.NET过去版本的相当一部分内容,才能使最新版本以合规方式构建。若这些旧版本需修改才能合规构建,维护工作将再度陷入困境。

源代码构建引用包

类似 Microsoft.CodeAnalysis.CSharp 的案例不胜枚举。每当项目针对低版本目标框架(如 net10 构建中的 net9),系统就会恢复低版本引用包。SDK工具(编译器、MSBuild)针对的通用.NET包版本需与Visual Studio自带版本匹配。那么我们该如何应对?若不彻底改造产品,无法简单地统一所有组件引用版本。

源代码构建团队发现,此类使用场景大多属于“仅引用”类别的包。

  • 当项目使用与SDK主版本不匹配的TFM进行构建时(例如使用net10 SDK构建net9项目),SDK恢复的目标程序包不包含实现代码。
  • 对旧版Microsoft.CodeAnalysis.CSharp的引用仅涉及 表面层 。这些包不会 重新分发 任何资源。若无需实现代码,可直接替换为仅引用包。

由此诞生了dotnet/source-build-reference-packages。仅引用包的创建与构建过程显著简化,同时满足构建过程中的用户需求。对于无需实现功能的包,可生成其引用包源代码,并构建基础设施在源代码构建过程中存储、构建并提供这些包。支持多版本实现相对简单。dotnet/source-build-reference-packages 仓库在 .NET 构建过程中生成,随后消费组件将恢复并基于提供的引用表面进行编译。

那么那些非引用案例呢?

在解决了引用包的问题后,我们可以将注意力转向其他不符合源代码构建规范且不属于“引用”类别的输入。主要分为三类:

  • 闭源或无法从源代码构建的输入——优化数据、Visual Studio集成包、内部基础设施依赖项等。
  • 遗留依赖——依赖于旧版.NET实现的开源组件
  • 平台交叉依赖——依赖于其他平台实现的开源组件

下面我们具体探讨如何处理这些情况。

闭源/无法从源代码构建的输入

闭源或任何无法从源代码构建的输入在Linux发行版维护者构建中绝对不可接受。为解决此类情况,我们需分析每项使用场景以确定处理方案。请谨记我们的目标是为发行版合作伙伴提供符合规范的构建实现,其功能应尽可能接近微软官方版本。例如:我们不希望微软的 Linux x64 SDK 与 RedHat 的 Linux x64 SDK 在行为上存在实质差异。这意味着 Linux x64 的运行时和 SDK 布局需尽可能保持一致。值得庆幸的是,相当一部分闭源使用场景并非生成功能等效资产的必要条件。示例:

  • 我们可能恢复某个启用签名的包,该功能在发行版合作伙伴构建中并非必需
  • dotnet/roslyn 存储库构建的组件为 Visual Studio 提供支持。这些组件依赖于定义 IDE 集成范围的 Visual Studio 包,但此类 IDE 集成功能并未包含在 .NET SDK 中。通过调整构建过程,可在源代码构建中“剔除”该功能,此类操作较为常见。

若无法在不改变产品功能的前提下精简依赖项,我们还有以下方案:

  • 开源依赖项——通常可将闭源组件(或至少满足特定场景所需的关键闭源部分)开源。
  • 调整产品行为——有时团队可通过有意设计变更消除产品差异。请注意关键要求:所有发布在发行版合作伙伴软件源中的内容都必须基于源代码构建。这允许部分资源动态引入。可将其类比为NPM包生态系统与NPM包管理器的区别。发行版可能选择从源代码构建NPM包管理器,从而让用户在构建时动态恢复NPM包。
  • 接受行为的细微差异——此类情况极为罕见。在 .NET 10 之前,WinForms 和 WPF 项目模板以及 WindowsDesktop 并未包含在源代码构建的 Linux SDK 中,尽管它们在微软的 Linux 发行版中可用。这是因为在非 Windows 平台上构建这些存储库所需部分存在困难。
遗留依赖项

我们已经讨论了如何处理闭源和不可重现的依赖项。那么遗留依赖如何处理?首先需明确“遗留”依赖的定义:如前文所述,产品中存在大量“不一致性”。项目可能为多个目标框架构建,并重新分发旧版.NET的资源,这完全是为了支持重要的客户场景。但构建所有版本的组件并不可行,此时我们的单一实现规则便发挥作用。我们选择每个组件的单一版本进行构建并随产品发布。允许通过 dotnet/source-build-reference-packages 引用旧版本,但禁止依赖旧实现。

首先,我们寻求规避依赖的方法。该依赖对正在开发的 Linux SDK 是否必要?若非必要,可从构建流程中移除该代码路径。若确实需要,能否统一为单一实现方案?多数情况下,不一致性源于产品组件以不同速度推进依赖项升级。万不得已时,可探索涉及行为差异的折中方案,但我们力求最大限度避免此类情况。

组合与垂直性

连接是最后一批需要移除的预构建组件。它们的产生源于产品内部依赖关系最终在其他环境中构建。例如,我可能在Windows上运行构建任务,为全局工具生成NuGet包,但构建该NuGet包需要Mac、Linux和Windows平台的原生适配器可执行文件。这些适配器只能(合理地)在Mac和Linux主机环境中构建。此类依赖关系表明产品构建更趋向“交织式”而非“垂直式”,在多仓库产品构建图中往往随时间自然形成。该图中的每条边都代表一个序列点——此时所有先前节点的输出均已可用,无论其构建位置如何。只要存在依赖关系,它就会被建立。

然而,分发合作伙伴的构建必须满足单平台且单次调用的要求。尽管存在引导过程,他们希望在拉取依赖项后断开网络连接直接执行构建,最终产出全新的.NET SDK。跨平台依赖关系阻碍了这种操作模式,它们阻断了“构建垂直性”。请记住连接点。当我们基于源代码构建模型开始实施微软统一构建时,将需要重新审视这些连接点。

对于源代码构建,我们处理连接点的方式与处理传统依赖项类似。关键要点在于:源构建专注于在Linux发行版合作伙伴构建环境中生成.NET SDK及相关运行时。因此我们尽可能消除依赖(例如在Linux上运行SDK时无需打包Windows全局工具可执行文件存根),并根据需求(如.NET工作负载清单)重构产品或构建流程。

愿景——构想统一构建§

统一构建旨在将Linux发行版合作伙伴Source Build的通用原则应用于微软发布的产品。实现这一目标将为Linux发行版合作伙伴、上游贡献者和微软带来重大收益,降低维护成本并提升快速构建与交付的能力。尽管我们从一开始就意识到,若不进行重大产品改动,可能无法完全复制Linux发行版的构建方法,但我们认为可以接近这一目标。.NET团队提出了以下核心目标(注:“.NET发行版维护者”涵盖所有参与.NET构建的团队,包括微软内部人员):

  • 单次Git提交应完整涵盖特定.NET构建版本的所有源代码,确保提交内容逻辑自洽
  • 单个仓库提交即可生成可交付构建版本
  • .NET构建系统应能在单一构建环境中生成特定平台的发行版
  • .NET发行版维护者应能高效更新和构建.NET(支持协作与独立模式),覆盖.NET版本整个生命周期(从初始提交到最终提交)。
  • .NET发行版维护者无需使用微软提供的服务即可生成下游发行版。
  • .NET发行版维护者应能满足其发行版的来源追溯和构建环境要求。
  • .NET发行版维护者应能协调下游发行版的补丁更新。
  • .NET发行版维护者可对构建后的产品运行验证测试。
  • .NET 贡献者应能轻松生成完整产品构建用于测试、实验等场景。
  • .NET 贡献者应能高效处理其负责的产品模块。

然而实现这一目标仍需解决大量新问题。让我们审视在将 Source Build 作为微软 .NET 构建系统前需解决的部分挑战。

建立产品最终内容的判定机制§

在分布式模型中构建产品时,产品的 构建验证 以及 构成 产品的实际内容都紧密关联。Source Build基于最终的连贯图结构对扁平化源代码布局进行操作,但它依赖传统的.NET产品构建流程来确定每个组件在布局中出现的版本。要充分发挥其优势,我们需要一种方法,能够在共享源代码库中直接更新组件,而无需复杂的依赖流。否则,如果开发人员希望在运行时进行更改,最终将导致产品需要构建两次:第一次是将包含更改的运行时构建流经所有运行时路径,第二次则是使用该新运行时构建产品。

现有方案

图5:微软:重新构想 .NET 的构建与发布方式(再次) 高亮路径展示了在分布式构建模型中,运行时变更如何通过多个仓库级联传播,需要顺序构建和依赖流更新。

需求说明

图6:微软:重新构想 .NET 的构建与发布方式(再次) 高亮路径展示运行时变更如何即时流向源代码布局。我们称之为“扁平化流”

提供应对破坏性变更的机制§

扁平化流程显著减少了变更传播的跳转次数,从而降低了变更进入共享源代码布局过程中的复杂度与开销。值得注意的是,变更在进入产品前仍需经过PR验证,并可能接受更深入的滚动式CI验证。然而,假设该变更需要消费组件作出响应。尽管依赖流程已转变为扁平化模式,ASP.NET Core仍依赖于.NET运行时。而布局中的ASP.NET Core代码并不知晓新的运行时变更。任何在变更被允许进入共享源代码布局前的PR验证都必然失败。

在传统依赖流系统中,我们通过依赖更新PR处理此类情况:若API变更导致构建失败,开发者需在PR中修改(理想状态),验证通过后PR方可合并。要使单一源方法论适用于.NET,我们需要在dotnet/runtime更新PR中修改 其他 组件的源代码。

提供基于仓库基础设施的验证机制§

如前所述,大量关键验证逻辑存在于组件仓库层级——这正是开发者投入时间的核心区域。将所有验证逻辑迁移或复制至其他位置不仅可能造成资源浪费,更将带来高昂成本且难以维护。若无法依赖依赖流在组件进入共享源代码布局前完成验证,则需建立事后验证机制。

为解决此问题,可将新产品构建的所有输出回流至各自仓库,并与Version.Details.xml文件中的依赖项匹配。这意味着dotnet/aspnetcore将接收大量新构建的.NET运行时包,dotnet/sdk将接收新构建的ASP.NET Core、.NET运行时及Roslyn编译器包等。这些构建将通过仓库基础设施验证输入依赖项的“最后构建版本”。

图7:微软:重新构想 .NET 的构建与发布方式(再次) Backflow 提供了一种验证近期构建的 .NET 输出与仓库基础设施一致性的方法

实现双向代码流§

假设某个运行时插入PR修改了System.Text.Json中API的签名。当前向流转时,负责开发者需更新所有下游用户的签名。假设这些代码位于src/aspnetcore/ *src/windowsdesktop/* 目录下。新产品构建完成后,包含新版API签名的更新版System.Text.Json包将回流至dotnet/aspnetcoredotnet/windowsdesktop。此时main分支的HEAD状态并未包含共享布局前向流PR中的直接源代码变更。开发者需要手动将这些变更移植到源代码仓库,通过回流PR进行修改。这既繁琐又容易出错。我们的新系统需要提供一种机制,能够自动将共享布局中的变更回流到源代码仓库。

图8:微软:重新构想 .NET 的构建与发布方式(再次) 组件变更流向共享源代码布局,仅在共享源代码布局中进行的额外变更会通过支持包回流至组件仓库。请注意,这是共享源代码变更的通用回流功能,不仅限于正向流PR中的变更。

提供更完善的插入时验证§

回流验证并非完美无缺。它无法为依赖组件中的不良变更提供便捷的 合并前 门控机制。我们可通过识别并弥补仓库测试中的漏洞来缓解此问题——这些漏洞曾导致不良变更在源仓库中被合并。同时需承认部分问题始终难以完全杜绝,高质量产品的打造过程绝非仅凭PR通过测试即可实现。许多仓库无法在合并前运行完整测试套件。但我们 也可 针对刚构建的产品投入场景化测试——这正是传统依赖流系统所欠缺的能力。

任何完整产品场景测试都依赖组件更新到达dotnet/sdk仓库。在此之前,我们无法获得可测试的完整.NET产品。任何尝试都只是某种“拼凑构建”。 注:大量端到端测试仅以dotnet/sdk仓库层级的PR/CI测试形式存在。 但变更需经过漫长传播才能在测试中显现效果。

源代码构建方法论确保每次组件变更都会触发完整产品构建,无论该组件在产品构建图中的位置如何。这意味着我们能在每次插入变更时创建并运行全面的测试套件。这些测试应聚焦于覆盖产品功能的广阔领域。若测试通过,则可合理预期.NET正以支持开发持续推进的方式运行。

提供构建.NET完整发行内容的途径§

Linux发行版的源代码构建服务仅专注于1xx系列SDK、ASP.NET Core及运行时内置组件,构建支持这些布局的软件包。正如先前预构建消除策略所示,这种聚焦是满足发行版合作伙伴构建要求的关键。若要构建微软发布的完整内容,则无法维持如此狭窄的范围。

扩展范围在某些领域较为简单,在其他领域则困难重重。从某种意义上说,我们只需放宽限制,将更多功能重新纳入构建流程。需要允许从源代码库恢复预编译二进制文件(如签名功能),并构建所有TFM文件而非剔除.NET Framework目标。我们需要构建那些原本被排除在源代码构建聚焦共享源代码布局之外的组件,例如 Windows 桌面、Winforms、WPF、EMSDK 等。更具挑战性的则是并行构建。需注意Linux发行版的源代码构建采用单布局、单机器、单调用的模式。这种模式足以生成布局文件,但.NET中存在大量其他构建产物需要多台机器协同完成,这打破了单机器垂直构建的概念。

理想情况下,我们应重构产品架构以避免这些连接点。但这往往需要牺牲客户体验或增加产品复杂度。在企业级产品中,即使跨主要版本迭代,简化 SDK 而不破坏客户现有系统也极具挑战。历史决策深刻影响着未来可选方案。最终我们只能通过产品构建实践消除可控的并发点,而残留的并发点将成为必须接受的现实。构建流程必须通过多阶段构建机制实现跨机器运行。

愿景落地——统一构建方案落地§

统一构建项目大致可分为四个阶段:

  • 初始构思与设计(.NET 7)——统一构建项目的初步设计工作始于2022年初的.NET 7开发阶段,历时约4个月完成。该项目于2022年下半年获得全面启动批准,计划在.NET 9 RTM版本前完成,期间设有多个关键决策节点,可在这些节点终止项目仍能实现基础设施的净收益。
  • 基础建设阶段(.NET 8)——.NET 8版本期间,统一构建项目专注于基础架构优化,旨在提升源代码构建系统的可持续性,并开发支撑完整构建流程所需的功能模块。这些投入即使在概念验证阶段发现重大未知问题导致方向调整时,仍能为.NET整体带来净收益。
  • 垂直构建/代码流探索(.NET 9早期)——基础工作完成后,我们着手为三大操作系统家族(Mac、Windows、Linux)分别实现垂直构建。此举旨在尽可能提前识别产品化阶段需解决的问题。我们尤其关注发掘任何未知的产物构建连接点。同时,我们对代码流与代码管理方案进行了深度研究,最终验证并确定了下文所述的实现方案。
  • 产品化阶段(.NET 9后期至.NET 10)——经历春季至夏季的延期后,最终实现方案于.NET 9尾声正式启动。延期导致发布日期推迟至.NET 10,这反而成为意外之喜。额外获得的6个月打磨期使我们得以在.NET 10预览版/候选版周期中期(预览版4)启用统一构建流程。虽然.NET预览版4搭载了新构建流程,但仍沿用旧代码流。预览版5引入了新代码流,此后我们便再未回头。在随后的数月里,开发工作流得到进一步优化,构建与代码流流程也获得了更充分的打磨时间。

历经近四年的构想与耕耘,统一构建系统终于随.NET 10正式版问世!

下面让我们了解该项目的核心组件。

VMR——虚拟整体化仓库§

dotnet/dotnet VMR(即“虚拟整体化存储库”)是统一构建项目的基石。它作为源代码布局,支撑着包括Linux发行版合作伙伴在内的所有.NET构建工作,堪称协调中枢。功能上,它与.NET 8.0之前的源代码布局并无本质差异。该布局现已正式迁移至Git仓库(而非源代码压缩包)。此举至关重要,它使开发者既能在各自组件仓库中进行精细化的开发工作,又可在需要跨组件变更时参与VMR协作。如此既能享受分布式仓库体系的多数优势,又可避免一致性问题。

垂直构建§

垂直构建是.NET转向按垂直领域生产资产的战略转折。所谓垂直领域,即指在单台机器上通过单条构建命令,在无需其他领域输入的情况下构建.NET产品部分组件的模式。通常我们按目标运行时划分垂直构建,例如Windows x64版、MonoAOT版、Linux arm64版、PGO配置版Windows x86版等。总计存在35-40种不同垂直构建。我们将这些划分为“短堆栈”和“高堆栈”:短堆栈仅构建运行时环境。高堆栈则构建完整的SDK。

最初设想是将所有并行垂直构建的输出合并,即可获得.NET所需的全部发布内容。这种架构本应高效且便于上游合作伙伴协作。遗憾的是,.NET产品设计多年来已固化若干必需的合并环节。例如,.NET工作负载包必须调用多个操作系统构建的众多组件才能完成构建。为解决此问题,我们最终增加了两轮额外构建。好消息是这些额外构建仅涉及精简后的垂直领域,且每个领域内组件数量也大幅缩减。虽非完美方案,但尚可掌控。

代码流§

统一构建项目最有趣的方面,或许在于其代码流的管理方式。这正是.NET略微颠覆传统开发模式之处。如前所述,要将产品维护为相互依赖组件的图结构,同时将代码流扁平化为共享的统一布局,就需要实现“双向”代码流。变更既需从组件流向共享布局,共享布局中的变更也需能回流至组件仓库。从概念上讲,代码流算法的复杂度并不高于单个Git仓库中可建模的流程。关键在于如何在没有关联Git历史记录的仓库间实现这种交互。

注:该算法的具体细节将由其他团队成员在后续文章中阐述。待文章发布后,我将更新本文添加链接。

现阶段让我们聚焦基础原理:

VMR与组件仓库均会追踪来自对应伙伴的最新代码流向。该信息与标准依赖关系数据共同存储于eng/Version.Details.xml中,当然也可设想将其存放于其他位置。

核心思路是比较“最后一次流”与当前流入内容的差异。以最简单的情况为例:当dotnet/runtime发生新提交,而VMR中的src/dotnet/runtime未作修改时,依赖流系统将执行以下步骤:

  1. 确定两个计算差异的基准点A和B。本例中,点A为dotnet/runtime最后一次提交至VMR(或当前处于PR状态)的流程,点B为dotnet/runtime的新提交。
  2. 构建补丁文件,将src/runtime文件映射到VMR的目录结构。
  3. 基于差异结果创建PR。可参考正向流示例反向流示例

.NET 8和.NET 9采用单向代码流的VMR机制。这类无需双向修改的场景简单且稳定。当开发者开始双向修改,且依赖流随时间动态变化时,情况就变得复杂了:

  • 差异点的计算变得更具挑战性,需要明确“最后一次流向”的方向。
  • 并入冲突不可避免,需采用开发者可理解的方式处理。
  • 代码流源目标的变更可能引发严重混乱,需要强大的错误处理和恢复机制。

关于代码流的讨论暂且到此为止,敬请期待后续内容。

场景测试验证§

统一构建的最后一个核心支柱是增强场景测试。需要明确的是,.NET 并非缺乏测试能力——若可行且务实,.NET 运行时完全可以耗费数月机器时间来验证每个 PR 中的数百万项测试。我们的审批、构建、验证和签核流程确保了高质量的交付成果。然而在虚拟机运行环境(VMR)中直接修改代码时,扁平化流程会引入新的延迟——即从修改完成到针对每个VMR组件进行深度验证之间存在时间差。虽然无法在PR和CI阶段运行所有测试,但我们意识到更完善的自动化场景测试能有效防止回归问题。我们的目标是新增覆盖广泛产品功能的测试,这些测试不直接关联构建系统或仓库基础设施,而是针对最终构建产品执行。若场景测试通过,则表明产品功能处于良好运行状态,贡献者不会因此受阻。

成果§

那么,.NET团队历经近四年的构思、规划与艰苦努力,最终收获了什么?如此庞大的投入是否值得?事实证明,我们获得了丰硕成果。

让我们先从最显而易见的成果切入,再深入探究其背后的机制。

灵活性、可预测性与速度§

迄今为止,我们获得的最大投资回报在于灵活性。分布式产品构建效率低下,生成一致的构建版本耗时漫长。提交新修复或内容时需协调配合以避免“重置构建”——因为在分布式开源生态中,交付内容与构建方式紧密相连。采用新修复方案可能导致无法及时交付验证。扁平化流程消除了这种一致性问题,实现了“内容”与“方式”的分离。在推进RTM版本或服务更新时,这种设计价值非凡。它意味着我们能在发布周期后期进行修复,将更多精力聚焦于修复是否达到服务标准,而非纠结于能否实际构建并交付变更。这种灵活性对客户大有裨益。

部分灵活性源于构建速度的提升。这听起来可能慢如蜗牛(.NET本就是庞大复杂的产品),但.NET已设定目标:未签名构建4小时内完成,签名构建7小时内完成。相比.NET 8.0和9.0的漫长周期,这已是显著进步——即使一切顺利,8.0或9.0的构建仍可能耗时24小时。7小时完成签名构建意味着每天可验证约3次滚动更新的.NET资产。构建时间的提升主要源于开销的消除

部分灵活性还源于可预测性。分布式产品构建涉及更多变量,需要更多人工干预点,系统和流程的故障点也更多,这往往导致结果难以预测。在分布式系统中,“ 如果我向dotnet/runtime提交修复程序,何时能获得可用的构建? ”是个难以回答的问题。我知道dotnet/runtime构建需要多长时间,但这个变更何时能通过依赖流传递到下游?届时是否有人值班审核批准?下游PR/CI验证状态如何?在我们获得完整构建前,是否会有重要变更合并到dotnet/aspnetcore分支,导致验证流程倒退?在.NET 10中,这个问题要简单得多:变更流入VMR(或直接在VMR中完成)后,将在下个构建中体现,而下个构建耗时N小时。

基础设施的稳健性与完整性§

在耀眼的指标背后,是多年对基础设施进行的生活质量改进,这些改进日复一日地带来巨大回报。.NET 8 对源代码构建基础设施的优化,显著降低了 Linux 发行版源代码构建的维护成本。此前成本主要源于变更提交后,需等待其流经构建图并抵达共享源代码布局时,才可确认是否破坏构建——这种延迟耗费大量资源。在预览版中期前,源代码构建的 .NET SDK 往往无法达到“预构建干净”状态,也无法由发行版合作伙伴交付。.NET 8的基础设施改进使我们能在PR阶段更轻松地识别新的预编译输入——此时问题更易诊断和解决,且尚未进入源代码布局。如今我们实现了100%的预编译清洁率。这减轻了源代码构建团队的负担,使其能腾出精力投入其他领域。新增的并行构建、更可预测的依赖流、增强日志记录、消除冗余复杂性……改进不胜枚举。这些投入正是成就产品成功的关键。

我们的签名工具需全面改造,以支持多平台多种归档格式的签名。若无此项工作,统一构建就无法实现。但这项扩展支持带来的收益远不止核心.NET产品。众多附属代码库因此简化了构建流程,避免了在Mac/Linux与运行签名工具的Windows机器间反复传输代码。构建开销降低,流程更快速简洁。

未来方向§

那么统一构建项目接下来将走向何方?虽然我们不会在.NET 11上投入同等规模的资源,但我们将针对性地优化基础设施,以提升开发人员的工作流程和用户体验,重点围绕代码流进行改进。其中令我特别兴奋的是AI代理技术——这些智能助手能监控代码流,串联起产品创建过程中涉及的各个系统,从而精准定位问题。从PR到产品发布的过程中,涉及众多系统与参与方(Azure DevOps、GitHub、代码流服务及其配置、代码镜像、开发者审批、机器分配等)。当流程顺畅时一切正常,但一旦出现问题,往往需要人工追踪事件链的具体故障点,这既繁琐又耗时。现有工具虽能辅助,但本质上仍需人工串联大量环节。虽然可编写规则引擎解决,但我预感这会导致系统脆弱且复杂。而具备模糊感知能力的智能代理才是此类任务的理想选择——减少繁琐操作,打造更优质的.NET。

最后,在.NET 11之后,消除连接点的努力可能迎来新阶段。其优势显而易见:更简洁、更快速、更友好。我们已精确测算出消除剩余连接点后的构建速度(不足4小时)。

结论§

若您读至此处,谨致谢忱!本文旨在揭示.NET构建与发布机制的运作原理。您已了解到,分布式依赖流产品构建模型未必能确保软件的可预测性与可靠性交付。这类系统往往存在高复杂度与高开销,导致时间成本增加。您还读到了.NET统一构建项目的起源——源自.NET Linux发行版的源代码构建,以及将这些理念应用于.NET时遇到的挑战。最后,你了解了.NET如何实践这些理念,以及我们在日常工作中见证的显著改进。

详述扁平化代码流算法的博文即将发布,敬请期待!

本文文字及图片出自 Reinventing how .NET Builds and Ships (Again)

共有 135 条讨论

  1. 我对.NET团队怀有极大的敬意。他们经常发布精彩的深度文章,对性能的追求永无止境(例如Kestrel和Entity Framework的演进历程)。

    而ASP.NET是少数几项成功经受重大破坏性变更考验的大型项目之一,其难度几乎可比拟Python 2到3的迁移。若你的Web应用依赖其神奇的会话机制(该机制竭力保持前后端状态同步),则必须彻底重构应用行为。

    当有3万亿美元的巨头真心关切并致力于优化你使用的技术栈时,这种感觉真好。

    开发者!开发者!开发者!

    1. 虽不确定具体数据,但可以肯定他们抛弃了海量项目。这群人或许不会在HN上高调发声,但绝不代表他们不存在。

      即便在新项目中,我仍会遇到迫使我使用4.8版本的问题。比如构建Excel公式时,强制采用的async/await方法根本行不通——这本非异步操作,若在UI上下文中等待反而会导致死锁。他们还破坏了大量Windows集成功能——在企业环境中,相同语法的网络调用在4.8版本能成功认证,但在核心版本却失败。

      由于众多库的向后兼容性遭到破坏,将复杂代码库迁移至核心版本需要付出巨大努力。

      专注性能优化固然可贵,但.NET框架的功能层面已然僵化。标准库添加JSON序列化器耗时15年之久,更别提对新型图像格式(webp、heic)的支持。如今一切变得复杂不堪,Visual Studio里每天都能看到崩溃提示。我曾是.NET的忠实拥趸,但如今怀念Anders的领导时代。

      1. 关于图像处理…我认为最好将大部分功能移出核心…或许可以开发一个NuGet包,内置ImageMagick、OptiPng等工具,用于通用图像操作。.NET在此领域的体验向来欠佳。

        至于破坏性变更… .Net Core 1.0发布已近十年… 我理解有人仍想继续使用Windows 7,但世界终将向前,你只能选择升级或继续使用过时版本。

        转向Core及拆分模块的根本原因,在于需要更好地支持Windows以外的平台… 否则.Net本身很可能早已消亡。近十年我参与的多数.Net项目都部署在Linux/Docker环境中…若非Core/5+的转型,这些项目本该完全转向其他语言/工具集。

      2. > 例如构建Excel公式时,强制使用的async/await方法根本行不通。这本就是非异步操作,若在UI上下文中等待将导致死锁。

        上次我做Excel互操作时基于COM实现,完全不涉及异步。不知你是否也使用了COM互操作?另外,async/await本就是为Winforms/WPF这类单线程UI环境设计的…

        > 标准库添加 JSON 序列化器花了整整 15 年…

        这说法并不准确。DataContractJsonSerializer [0] 早在 2007 年的 .NET 3.5 就已加入。诚然当时实现欠佳,但至少存在且可用。更何况当时 JSON.Net 已问世,且始终表现优异。

        > …更别提对任何新型主流图像格式(webp、heic)的支持了。

        Windows平台的图像支持历来由WIC[1]提供,它确实支持你提到的格式。但你说得对,原生.NET对许多图像格式的支持确实不存在。

        > 由于他们破坏了众多库的向后兼容性,将复杂代码库迁移到核心框架绝非易事。

        这点深有体会。我所在的公司仍有基于.NET Framework的代码库(编译于4.5.2但部署在4.8环境),该系统基于WCF构建,迁移到核心框架时遭遇了巨大断层。但最终我认为这种断裂是明智之举。在向现代多平台框架的重大跃迁中,过多的设计缺陷、错误假设和底层系统变更使得兼容性难以维系。如今的.NET比.NET Framework更快速、更灵活且功能更强大——即便实现这一转变耗费了漫长岁月。

        况且,即使.NET Framework不再新增功能,微软仍将支持.NET 3.5.1至2029年![2] 22年的支持周期难道还不够吗?(.NET 4.8的终止支持日期甚至尚未公布!)

        [0] https://learn.microsoft.com/en-us/dotnet/api/system.runtime…. [1] https://learn.microsoft.com/en-us/windows/win32/wic/native-w… [2] https://learn.microsoft.com/en-us/lifecycle/products/microso

        1. 4.8版本基本不会消失。它被内置到Windows系统中,这要归功于Longhorn开发团队——其中许多成员中途离职加入了谷歌。

        2. 关于编写Excel公式,虽然理论上可使用VSTO/COM实现,但性能表现会很差。正确做法是采用ExcelDNA/ManagedXLL。但Excel计算引擎从未设计支持async/await,所有运算都在UI线程中执行。因此若需在Excel函数中进行网络调用,很快就会陷入无解困境。

    2. 上次尝试Entity Framework时速度很慢。后来用Dapper和简单自定义迁移系统替代,在低配硬件上使用SQLite时,启动阶段的数据库验证和初始化时间从10秒缩短到2秒以内。Entity生成的查询语句存在冗余的多重连接语句级联问题。

      如今我更倾向于使用工具链简单的Go语言配合HTTP后端。.NET更适合其他类型的解决方案。

      他们的框架问题太多,比如WPF需要实现Win32黑客技术。例如.Net 9是首个能正确返回所有网络接口的Windows版本,旧版运行时只暴露已启用的网卡。某些方案我仍需维护Windows 7支持。

      1. 我反其道而行,将大型项目中所有Dapper+SQL及仓库层彻底替换为EF Core 10。性能未见变化,却清除了数千行冗余代码。更紧凑的代码令人满意,但必须时刻警惕EF的“魔法”特性,避免其执行那些不易察觉的怪异操作。

      2. 我们在多个大型项目中使用新版实体框架(Core)从未出现问题。甚至已弃用Dapper——它除了生成字符串和大量SQL外毫无价值。

      3. EF Core在配合变更追踪器进行简单查询和数据修改时表现出色。可通过AsNoTracking/投影实现类似Dapper的查询功能。采用命令查询分离模式时,也可将查询交由Dapper处理,命令部分交由EF Core负责。

      4. 我们也遇到越来越多EF的性能问题。虽然有调优方法,但不确定是否值得为EF学习这些技巧,或者直接使用纯SQL是否更好。微软似乎总爱创建无法100%生效的抽象层,.NET也有类似问题。对于与Windows和硬件紧密耦合的应用,往往需要直接调用Win32接口。

        1. 您指的是EF还是EF Core?如果是后者,是否启用了自动属性评估/延迟加载等特性?

          EF Core默认性能相当出色且功能基础。需要手动调用.Include()进行连接操作等,因此很难出现性能瓶颈。

          1. EF Core。在一定程度上表现良好,但涉及大量连接的大型查询可能变得非常缓慢。并非全部,只是其中部分情况。

        2. 这不正是所有ORM的核心问题吗?归根结底,性能优化时还是得直接写SQL。

      5. EF Core的诀窍在于:简单操作交由它处理,但若需要比.Include更复杂的逻辑,就自己编写查询语句。

  2. 文章写得不错,但.NET团队显然该放弃AzureDevOps——队列等待时间才是主要瓶颈。直接运行裸机构建服务器才是正道。或许存在不这么做的理由,但这篇文章避开了显而易见的问题。

  3. 现代.NET确实很棒。过去三年里,我在macOS上用VSCode开发C#的REST API后端,部署到Linux运行,从未出过问题。我用SQLite、EFCore和Minimal APIs,相比前端部分简直是享受——后者用的是NextJS/React/MaterialUI,npm里挂着50多个(开发)包。

    1. 若需稍复杂的实现,建议尝试FastEndpoints库…目前它是我最钟爱的API解决方案。

      次优选择是TS + Hono + Zod-OpenApi搭配SwaggerUI…但配置上下文类型稍显繁琐。

  4. 令我印象深刻的是,这项工作的根基在于Linux发行版构建系统。换言之,他们为实现.NET开源与跨平台所付出的努力,最终让所有人的工作都变得更轻松。

    1. 本文作者在此说明:

      我们希望发行版维护者能将.NET纳入原生发行包源。这意味着构建工作需由发行版维护者而非我们完成,因此必须创建符合其需求的构建系统。结果要么形成两个并行的构建系统,要么尝试统一。唯一可行的方向是采用Linux发行版模式——尽管这是最严格的方案。

      好消息是发行版模型更简单。它可能不是最高效的方案——采用缓存机制的分布式系统会快得多,但这种方案既难以实现,也不符合发行版维护者的工作流程。在此情境下,优化简单性才是更优选择。我们希望社区能以有意义的方式参与建设:为BSD构建、为S390x构建、构建并纳入发行版源等。但我们无法实际支持社区期望的所有平台和场景。

      1. 很高兴看到这篇讨论。长期以来我对.NET的担忧在于,相较其他语言生态,它存在仅微软内部人员掌握构建与移植技术的风险。是否有发行版实际提供从源代码构建的.NET SDK包?

          1. 感谢提供参考。我记得以前安装 SDK 需要安装微软仓库,这确实是个改进。

        1. 是的,Canonical、RedHat、Fedora、Alpine、Centos 和 Arch Linux 都支持。NixOS 和 nixpackages 同样支持。

          1. 看来我离开dotnet期间错过了不少进展,感谢分享。我至今还记得当年必须安装微软仓库才能实现的功能。

    2. 我认为.NET早已超越了质疑其开源举措的阶段。过去十年间涌入的拉取请求及其带来的积极成果(是啊,转眼已十年)实在令人震撼。

      1. 没错,我并非指这证明开源举措成功或有价值,而是说明投入开源的工程努力也为整个项目带来了回报。

  5. 哇哦,没想到今年读到的最棒的软件工程文章竟出自微软之手!别误会:我确实喜欢.NET,尤其近期版本,但直到此刻我仍以为它的稳健性不过是逆势而行的幸运逃生——在行业普遍走向糟糕化的背景下,它像个低调的奇迹般幸存下来。

    读到这篇详述协同努力的文章(配有图表,甚至给出了智能体式LLM应用的真实案例!),描述如何 切实有效地改进事物 ,简直令人耳目一新——尽管文末提到未来不会再投入如此惊人的质量资源。

    即便你对.NET和/或微软毫无兴趣,这篇文档也值得一读——若你正负责任何领域的系统重构,其价值更是倍增,这才是真正的实践典范!

  6. 我理解复杂系统的高层级概览有助于洞察全局,但同样感到这种抽象化的组织思维正是问题的根源。当你将复杂系统简化为抽象组件时,必然反复遭遇困境——因为你主动忽略了那些棘手的细节。自上而下的方法试图解决所有问题,但自下而上的方法甚至能根除无数问题。

  7. > 我们想知道每月构建3-4个主要版本(期间包含十几个.NET SDK版本)需要多少成本。

    为何需要这么多变体?

    1. 首先存在.NET 8(长期支持版)、.NET 9(标准支持版)、.NET 10(长期支持版),这些版本需同时提供支持。

      其次涉及.NET SDK/aspnet/运行时(需适配x64/arm32/arm64架构的Linux/Mac/Windows平台),以及各类SDK包本身。

      1. 3××4=81个构建版本——但这些版本不都是独立的,因此可以并行构建吗?

        1. 不,请阅读原文。它需要先构建一些“子”SDK才能最终生成完整的SDK包。这正是核心要点:他们希望实现这种构建模式。

  8. 在Node流行之前,.NET曾是后端构建的稳健选择(且.NET通常比Node更高效)。

    我希望.NET构建版本的频繁更迭只是暂时的,因为许多人可能正寻求回归稳定版本——尤其是在近期Node生态系统遭受供应链攻击之后。

    1. > 我希望.NET构建版本的频繁更迭只是暂时的,因为许多人可能正寻求回归稳定版本——尤其在近期Node生态遭受供应链攻击之后。

      能否详细说明?这篇文章讨论的是.NET版本构建的内部机制。这与所谓的“频繁更迭”有何关联?

      1. 我的猜测是:若使用.NET Framework构建,程序可永久运行;但若源代码基于新版.NET,则需每年更新版本,并处理整个项目的升级工作——这意味着团队成员也需同步升级开发环境,同时还要应对语言和运行时的新特性、弃用项等问题。此外,许多包在版本变更时更新速度较慢,因此你很可能需要投入更多精力尽量减少依赖项使用(甚至完全不依赖),这可能带来大量额外工作。相反,若必须依赖某些组件,最好选择功能强大的瑞士军刀式工具。

        我认为Node.js更具灵活性,除非.NET Framework能恢复永久发布或提供更长期支持,否则它无法提供更好的权衡方案——毕竟你甚至无法获得更高的稳定性。

        1. > 若源代码基于新版.NET,则需每年更新版本

          .NET的发布周期设计令人耳目一新,与Node.js相似:

          – 每年11月发布新主版本
          – 偶数版本为LTS长期支持版,提供3年支持/补丁
          – 奇数版本提供18个月支持/补丁

          这意味着若选择LTS版本,你将获得2年支持周期,且有整整1年双版本并行支持期。若每次发布都升级,至少有6个月的兼容期

          各版本间几乎不存在破坏性变更,且变更多集中在基础架构(配置、启动流程、项目结构)而非实际应用代码。

          1. > 奇数版本提供18个月支持/补丁

            近期已通过延长至24个月支持期,消除了奇数版本的兼容性摩擦。

          2. 啊,但若使用 node.js,你可能每隔一天就会遭遇依赖项的破坏性变更——那些你甚至不知道存在的依赖项。

            1. 相比之下,.NET 的原生库(即使通过 Nuget 安装)稳定性高得多。

        2. 关键在于.NET项目能兼容旧版代码——这种兼容性近乎荒谬的程度。只要项目未跨越.NET框架分水岭,迁移至新版框架时基本无需修改代码,大多能直接运行。

          坦白说,.NET平台如今的稳定性已达历史巅峰。

          1. 从Core 1到2再到3的升级过程存在诸多不完善之处,但此后升级体验已相当顺畅。

        3. 近期实践报告:过去两周内,我将团队五项微服务中的四项升级至.NET 10。此前这些服务均运行在.NET 8或9环境。升级过程十分顺畅:针对.NET 9服务,仅需更新基础容器镜像和csproj目标框架;而.NET 8服务还需在集成测试中更新Mvc.Testing引用。

          我实在难以想象还有比这更轻松的版本升级。

          1. 我正将数十个项目迁移至.NET 10。目前所有项目基本只需修改一行代码并重新编译。

            从.NET 6升级到10几乎无需任何改动。

        4. 过去三年.NET升级对我而言完全毫无痛感。

        5. 当年.NET Framework尚未进入当前这种冻结状态时,每次发布都会带来一连串破坏性变更。而现代.NET的破坏性变更根本不值一提。不过紧跟技术前沿倒是更有趣…但这正是成为当下解决方案并保持相关性的必要条件。

        6. 注意.NET实践者如何赞美它,而非实践者(.NET Framework用户)如何批评它。

            1. 亦即那些敢于直面其缺陷的人。关于.NET的稳定性与健壮性存在大量社会评论,但不知为何,在我接触过的每个.NET解决方案项目中,总会听到“我们正在更新”的借口,而更新过程的神奇之处在于耗时往往超过实际功能的构建。

              或者其他技术栈里很标准的功能,在这里却需要耗费三个开发周期定制解决方案…当然,这其中肯定少不了各种漏洞。

              而且一直都是这样。不知为何,.NET总有一群信徒,他们始终将.NET奉为编程框架的巅峰。无论是.NET Core还是.NET Framework,都无所谓。

              几十年来,你总能听到同样的论调。

              只是实际体验和成果与这些宣称完全不符。

              在收到回复前,我必须声明:我对.NET的了解始于2000年代。

      2. 什么意思?过去十年间.NET生态系统根本就是一片混乱。

        几年前,就连多数活跃的.NET开发者都搞不清状况。现在好些了。我清楚记得.NET Framework v4.8发布后,几个月内.NET Core 3.0就登场了,同时宣布.NET Standard 2.0将是该版本的终结版——当时谁都摸不着头脑。

        .Net 5确实带来很大改善。即便如此,微软仍在以惊人速度发布新版本:如今我们已用到.Net 10,而.Net Core 1.0发布距今已有9年。近十年间几乎每年都有重大版本更新——这可是标准软件框架!v10作为软件框架的 LTS 版本,支持周期仅有 3年 。没错,它仅支持到2028年,而这还是LTS版本。

        1. 唯一混乱的时期是.NET Framework向.NET(Core)的过渡阶段。如今升级.NET版本基本无痛,因为破坏性变更通常只影响极特殊场景。对多数人而言升级只需几分钟。

          1. 除非你碰巧遇到那种特殊情况,那才真是糟心。

            在大公司从来不会几分钟搞定,所有变更都得验证,CI/CD管道要更新,现在遇到.NET 10还得等IT部门批准安装VS 2026。

            1. 说实话,若连更新/更换IDE的权限都无法获得,说明公司流程根本失灵。若CI/CD部门置之不理同样糟糕。

              1. 这在多数财富500强企业很常见——它们主营业务并非软件销售,开发工作多通过咨询机构完成。

                常见做法是通过Citrix/RDP/VNC分配虚拟机,还有专门的基础设施团队负责处理各类承包商的工单。

                1. 我之前的工作也类似。天啊,我们甚至还有个软件包只用32位.Net Framework 1.1构建。直到2018年左右,我们才开始因耗尽2GB地址空间而遭遇内存不足错误。

                  我欣赏.Net的新特性,但根据经验,许多.Net软件拥有庞大的代码库,且包含大量必须支持的客户定制修改。这些企业明确不希望其软件框架像当前.Net这样快速迭代主要支持版本,因为他们不能简单宣称“新版本理应正常运行”——光是处理所有重新验证工作,团队规模就得翻倍甚至翻三倍。

                  我再次恳请HN社区理解:并非所有人都身处25人的微服务初创公司。

                  1. 或许是我理解有误,但“绝不能破坏现有功能”与“测试需2-3倍团队规模”的组合,听起来像是发布陷入僵局——除非能完成测试。

                    在我参与过的迁移项目中,这始终是常规的工单/史诗级任务:在版本计划中安排迁移,执行迁移操作,完成其他预定功能开发,进行系统测试,修复所有故障,复测,修正,循环直至通过,最终发布。

                    否则你只能寄希望于完全掌握系统交互逻辑和潜在故障点——而我怀疑没人能做到。每个人都曾遇到过看似与自身变更毫无关联的故障。尤其在大型系统中,这种情况屡见不鲜。由于无人能掌握整个系统全貌,我们超过1%的合并操作会在意想不到的地方破坏夜间构建。

                    或者你只能通过外科手术般的精准操作,在每次发布前祈祷外科医生零失误,勉强维持一个垂死产品的生命。

            2. 你描述的只是大型僵化企业中的特定工作场景。这与.NET本身毫无关系,对吧?

              1. 猜猜大多数.NET开发者的雇主集中在哪些领域?

                1. 我对多数.NET开发者情况不了解。在我当前任职的美国某上市软件公司(数千名员工),升级时机由工程师自主决定。我们核心单体应用在首周就升级到了.NET 10。

                  1. 对我而言,客户的IT部门及其管理层说了算。

                2. 我从2001年底就开始使用.Net(包括ASP+),涉及政府和银行业务,本地开发环境的及时更新很少出问题。过去十年间,开发团队越来越可能掌控CI/CD环境,甚至部署服务器……不过我更倾向于容器化应用而非裸机部署。

            3. 关于迁移问题…请仔细阅读本讨论串的评论——评论数量众多,但无人提及任何实质性痛点,只有像你这样未实际使用过的人在讨论假设性问题。

              至于CI/CD管道… 我刚编辑了.github/workflow/目录下的文件来提升目标版本,一切顺利…不过若你部署的是裸机而非容器,确实需要额外几步操作。

              至于“安装权限”问题…这恰恰说明企业不信任那些本就掌握着决定公司生死的软件开发权限的员工… 开发者理应拥有本地管理员权限,或通过跳板机(远程桌面)实现…至少也该提供具备本地管理权限的Linux远程开发环境。

              我目前在政府机构的严格管控环境中工作,从未遇到问题。过去在大型金融机构任职时也是如此。

    2. 不确定此处为何用过去时。 .NET至今仍卓越非凡,且每次发布都在持续进化。你所指的不稳定性何在?当年向.NET Core的迁移确实存在重大兼容性问题,但那已是近十年前的事了。

      1. 若团队类似我曾共事过的团队,工程师们才刚适应摆脱.NET Framework的转型(!)

        无数开发者仅在Windows版Visual Studio中感到自在。而升级.NET版本需在多个界面间反复点击操作(Visual Studio安装程序、“获取新组件或功能”及NuGet包管理器)

        .NET Core的出现恰逢以下转型浪潮:

        * 转向云端,告别IIS和Windows Server
        * 转向Git,告别TFS
        * 转向远程CI/CD,告别“将文件拖入inetpub”

        * 转向单页应用,告别ASP.NET XAML编程(Blazor除外)
        * 转向更开放的工具集,使熟悉开源技术和开放规范成为优势,摆脱Visual Studio作为宇宙中心的地位(尽管它在IDE领域仍具统治地位)

        在深入.NET领域前,我来自Linux/Docker世界,因此在团队转型过程中既承受着怨气又被寄予厚望。多数队友从未阅读过.csproj或.sln文件内容,也未曾在终端执行构建命令并查看日志输出。当我协助排查问题时要求他们这样做,他们感到恼火;有人直接拒绝(“没必要研究VS内部机制”、“现代开发不该依赖DOS命令,VS应该自动处理!”)。

        我完全理解那些曾被承诺的开发者——他们以为对VS/IIS等技术的深度掌握将成为职业生涯中商业软件的坚实根基。而在技术更迭过程中,诸如“netstandard2.0将成为核心库及未来所有.NET运行时的永恒标准!”这类承诺,往往在次年就打上了星号。

        我百分百认同.NET开发团队的卓越贡献,但正是他们持续推动颠覆性变革——每当发现重大机遇就从根基重构——这种特质既成就了辉煌,也让部分人对其心存戒备。

        1. 感谢让我感觉自己像个十倍开发者 🙂

          总之,我同时使用.NET Framework和.NET。开发者的乐趣在于每日学习新技巧,满足求知欲。

          因此我难以理解为何有人无法在.NET生态中掌握新技能。对我而言这如同惊喜不断:哇哦,新语言特性让实现更优雅!哇哦,这段代码多么简洁!哇,ILogger让日志记录如此简单,还能自由接入底层日志引擎!通过JSON文件配置?太棒了!还能直接用环境变量覆盖?惊艳!这些都遵循特定规则和模式。哦,需要时我能随心定制配置读取方式!哦,只需简单命令就能从命令行构建运行!哦,还能打包到Docker容器在Linux上运行!哇哦,.csproj文件变得如此简洁,我的.NET Framework项目也必须采用SDK风格的项目结构了!

          1. 太棒了!而且没错,.NET Framework对某些工作负载依然至关重要,尤其是在需要深度调用Win32 API的应用中,C++/CLI和WCF能让.NET 4.0+的替代方案省去不少麻烦 🙂

            为缓和我的言论,必须承认这些深谙工具与系统的工程师们对我这个被招来推动现代化的新人产生抵触是情有可原的。最终他们从全面抵制转变为友善调侃——“那位Linux和命令行先生在此”——并接受了我的小脚本能解决Visual Studio与Jenkins/GitHub Actions自动化流程在Kubernetes运行时行为不一致造成的混乱与挫败感。

          2. 有趣的是…我其实有点讨厌ILogger…至少我见过的输出实现如此。我更倾向于用行分隔的JSON进行标准日志记录,同时为本地开发提供格式化版本。比如在Node项目中,我通常会附加少量上下文信息和包含简易日志消息的详细对象… 这种格式既便于传输到其他系统,在AWS等平台也能被内置日志系统完美处理。

            至今我还没见过哪个.NET日志器能做到同样出色,又不需繁琐的拜占庭式配置。

        2. 你对所有这些变更的看法完全正确,这些问题确实是.NET开发的顽疾。一举清除所有冗余代码堪称变革性突破。终于能抛弃IIS,将Web应用部署到运行紧凑型Web服务器(Kestrel)的Linux环境,实在令人振奋。

        3. 作为积极推动容器化部署、偏爱VS Code集成终端而非VS原生环境的人,我非常欣赏.NET整体的发展方向。过去几年我曾辗转于几个开发环境,那里的大多数开发者和项目都像被困在水泥里般寸步难行。正因工作环境令人窒息,我已先后离开过两家这样的公司。

          如今我甚至说不清如何在VS图形界面完成某些操作…只因Rider重构体验更佳,且曾多次修复VS启动问题(VS后端,vite/react前端),才同时安装了Rider和VS。

          在.Net Core出现前,我几乎要转向Node…现在我对两者都接受…不过我的所有Shell脚本现在都用Deno/TS编写了。

    3. 我热爱C#。搭配JetBrains Rider使用时,这可能是我职业生涯中最令人满意的开发体验。

    4. 自.Net Core 3以来,我几乎没遇到过直接影响我的重大兼容性问题…更新目标框架和依赖项通常相当轻松。虽然耗时稍长,但远不及更新那些搁置两三年的React项目那么痛苦。

      虽然现在框架的发布/LTS周期远比某些历经二十余载的版本短,但保持技术“前沿”其实并不困难。在我看来,这本就是“快速”软件开发周期中维护工作的一部分。企业需要数周而非数年规划的软件交付,这必然伴随着持续的维护工作。

    5. 我深有同感。作为多语言顾问,我渴望看到更多招标书要求.NET技能,可惜现实似乎只关注Node.js、少量Java以及大量低代码工具(iPaaS)。

      至少在性能问题上,我还能借此推动某些场景采用C++插件。

    6. >.NET曾是后端开发的稳健选择,直到Node大行其道

      社区的问题在于,这种论调在每个时代针对每个版本都被反复宣扬,尽管它根本站不住脚哈哈。无论.NET给解决方案带来多少弊病,开发者总会歌颂其真实或虚构的优点。

      这正是我至今回避与.NET团队合作的首要原因——这种现象至今未改。

      诚实会带来长远利益。

    7. 我热爱使用.NET开发,但最近更多用Python编写后端应用。代码更简洁,测试更轻松(因方法隐私机制名存实亡),部署更快速(无需编译)。

      这种情况可能改变,但根据我的经验,AI生成Python代码的效果优于.NET代码。

      1. 问题在于Python运行时效率低下。多数场景或许无碍,但我接触过许多初创公司因选择Python(或Rails、Node等框架)而遭遇严重可靠性问题——服务无法承受高峰负载,除非进行大量重构并增设应用服务器。

        根据框架不同,Python在techempower基准测试中比asp.net慢约3倍(FastAPI)至20倍(Django),这与我的实际经验高度吻合。

        1. 完全认同。刚完成FastAPI服务的负载测试。现在最大的卖点是:多数真实后端根本不会遇到需要考虑性能瓶颈的负载级别。

          1. 我在一家大型企业工作,其核心业务采用PHP编写的单体应用,主要采用服务器端渲染(SSR)架构。

            现代PHP令人愉悦,如今速度也快了许多,但性能仍是问题。它在25年前被选中时,开发者们肯定也以为永远不会面临如此庞大的负载。

            现代PHP几乎与.NET毫无二致,只是表面点缀着些PHP特有的风格。当年他们本该直接选择.NET。

        2. 我虽不常构建服务,但最近几个项目直接选择了Rust。缺点是开发效率较低——或许是我经验不足,但框架确实有待完善。不过我喜欢能在开发阶段发现并解决大部分问题。用Python建服务意味着我总在生产环境里修补漏洞。

          .NET确实比Python强,但相比Rust项目,它的类型系统和代码组织方式让我不太满意。

          1. > .NET确实比Python强,但相比Rust项目,它的类型系统和代码组织方式让我不太满意。

            你试过F#吗?

            1. 其实八年前我尝试过F#,当时非常喜欢,但未能持续深入学习到足以胜任工作的程度。现在工作中我仍用不少C#,加上我在Rust方面的经验(代数数据类型等),我认为F#能极大提升我们.NET代码的质量。

          2. 建议看看FastEndpoints库用于API开发…我认为它能显著提升开发体验…

            当然,Rust+Axum组合也很出色。

        3. 并非说这是唯一正确选择,但它能让更广泛的用户群体参与代码贡献,而像fastapi这类工具支持的快速迭代,在早期验证概念时至关重要。

          各取所需…况且水平Pod自动缩放器和负载均衡器的配置成本很低。

        4. 多数Web应用本就在等待数据库响应。我很少见到框架本身的运行速度能带来实质性差异。

      2. 若不想让方法成为私有,直接设为公有不就行了?

        1. 只需设为内部访问,并在程序集上使用[InternalsVisibleTo]即可。

      3. 我正从Python转向Java,因为Java能更轻松地调用所有CPU核心,严格类型检查能避免大量错误,且运行速度更快。我认为到2025年,Java的复杂度其实不会比Python高多少。

        1. 赞同。如今几乎人人都在使用超过8核的机器(即便是廉价安卓手机也普遍配备8核处理器),但人们对多核软件设计的认知却如此匮乏,这简直荒谬。

          在Python和Node中调用多核简直痛苦不堪,而.NET十多年来就提供了并行for循环和Task.WhenAll。Java在这方面类似——无需额外操作即可使用多核,可直接运行多个任务而无需担心“工作者”间状态传递等问题。

          这实际上对网页性能造成了严重影响——而性能优化正是我深切关注的领域。并非所有瓶颈都源于I/O阻塞,某些场景确实需要大量CPU资源来解决问题。当无法轻松实现并行处理时,最终将引发大量用户体验问题。

          1. 在多数云部署中,你获得的只是共享的“虚拟”核心——无论这意味着什么。

            1. 不,你实际获得的是你选择并愿意付费的数量。1vCPU几乎没什么用处。

          2. 连Guido van Rossum都承认,若早知高核心数CPU会如此普及,当初就不会选择GIL机制。

        2. 这正是我偏爱.Net的原因。加上AOT编译技术,简直完美。

        3. 好奇问一句,为何不选Kotlin?我以为如今它已是JVM语言的首选。

          1. 我用Kotlin做后端开发,但必须承认Java正迅速追赶,且Kotlin似乎已将重心转向Kotlin Multiplatform。现代Java是种优秀且易用的语言,也是更稳妥的选择。

            Gradle搭配Kotlin DSL很棒,但令人恼火的是Gradle总在无谓地重构API导致插件失效。某些插件还为追求花哨的DSL而引入毫无意义的破坏性变更。

            我认为IDE支持实际并非问题,因为IDEA是Java和Kotlin的最佳选择。官方Kotlin LSP虽已发布半年,但我尚未尝试。

          2. 我正在尝试学习并喜欢它,但Java代码实在太多了。每1个Kotlin示例对应着1000个Java示例。不过现在或许大语言模型能缓解这个问题。

          3. 糖分过高,而且没有JetBrains IDE的话,你只能用普通的文本编辑器。不确定这是否适用于普通的Kotlin,但学习Gradle Kotlin DSL时,试图理解底层机制的过程简直让人抓狂。

    8. .NET的迭代迭代比其他主流技术栈更平稳。自2017年发布的Core 2以来,每次升级都痛苦感极轻,近期更是毫无痛感。

      1. 我的体验类似,不过从Core 2升级到Core 3时确实很痛苦… .Net 5时遇到过些小问题,但之后就没什么值得一提的了。

        1. 它有后来者的优势(就像.NET之于Java那样)。

    9. 这其实不影响用户体验。只是再次印证了单仓库架构的优越性。

      1. 只要够狠地用子模块,任何项目都能变成单仓库哈哈

    10. .NET需要达到Node级别的开发体验和Rust/Zig性能,因为Node/Python生态系统的重构使其性能达到前所未有的高度

      说实话我看不到.NET能逆势取胜

      1. .NET的开发体验远优于Node,若为性能优化编写,其速度几乎可媲美Rust,绝对远超Node或Python

          1. 数据膨胀并非主动选择,而是被动结果。Node并非主动选择,而是前端工作量巨大的必然产物。而JavaScript之所以优秀,正是因为创造了C#的开发者为其打造了TypeScript。
            Python同样如此,它凭借数据科学和机器学习/人工智能的背景崛起。

            而普遍的负面因素在于微软这家企业本身。

            总结:关键不在技术本身,而在于行业生态。

            1. TypeScript的使用体验远不如.NET这类单调类型系统流畅,安全性也逊色许多。

              TypeScript虽将JavaScript引入现代语言领域,但个人认为仍不够完善。

          2. 能否提供你的测试数据及基准测试方法?我查到的所有基准测试结果都与楼上所述截然不同

          3. .NET在桌面端和Web后端领域的应用远比Java广泛。

            泰勒·斯威夫特是史上最受欢迎的歌手,但她就是最优秀且你最爱的歌手吗?

            人气固然重要,但本身并无实质意义。

            1. 我清楚Rust/Zig优于所有这些工具,且实际用户基数更小

              无需说服我,我早已同时使用这两种语言

              希望你能摆脱这些陈旧的技术栈

          1. Rust并非应用开发工具,它在驱动程序、渲染器等底层领域占据重要地位。
            Node和Python属于动态类型语言,最初定位于脚本编写场景,不适合多数需要专注核心功能的开发任务。

            .NET与Go、Java属于同一类应用场景。归根结底取决于你手头的开发者资源。

            关于生态系统:我精通.NET和JavaScript(更擅长浏览器端),可以明确告诉你:2025年的生态系统完全不成问题。早在2010年代就已成熟。

            关于开发者:现有团队是基础,后续招聘逐步补充。如同Java,.NET不会消失。好坏开发者皆有,这在任何语言中都存在。

            让我们少些教条主义。客观而言,.NET与Go、Java一样,都是各自领域中合适的竞争者,选择取决于现有团队/系统架构。初创公司则应遵循CTO的偏好。

            1. 我清楚.NET的定位:游戏开发和传统Windows应用。除此之外我们不需要它——更别忘了当HN用户被迫使用微软技术栈时有多痛苦。

              1. 多数.NET项目都是Web API和后端应用。

                1. “多数.NET项目是Web API和后端应用。”

                  若现在是2015年,你说的没错

                2. 实际差距达千倍之巨。难以置信。

  9. 为何微软的开发者部门堪称业界翘楚,而公司其他部门(除企业销售外)却成了无能与堕落的真实写照?他们是如何避免被蔓延的文化腐朽所侵蚀的?

    1. 主要归因于政治博弈和追随行业领袖。自比尔卸任后,他们早已不再照镜自省。

  10. 精彩绝伦的文章!

    这让我有动力深入钻研.NET 10了

  11. 我也靠C#谋生。可悲的是,语言糖衣和框架的过度膨胀让追新变得异常艰难。即便今天启动新项目,我仍选择.NET Framework 3.5及其语法。我知道这听起来极端,但3.5拥有构建优秀软件所需的一切。它还提供经过充分验证的环境。软件栈的配置过程极其简单。遵循v2运行时编写的程序也能在v4运行时运行,只需在可执行文件旁放置一个简单的配置文件,就能在任何Windows机器上运行,无需部署框架。

    1. .NET Framework 3.5 过于陈旧,在 Windows 系统中已不作为默认配置(最新版本可能完全缺失),开发者可能需要借助过时的工具链才能使用,且该版本很可能已停止支持并存在安全隐患。

      更不用说这种做法会严重限制技术实现的可能性。

      除非你身处停滞二十年的环境(考虑到这必然是微软生态,意味着严重的安全隐患),否则这简直是令人费解的荒谬策略。

    2. 3.5将在未来几年内终止支持,绝对不应以此构建新项目。现代.NET引入了大量提升开发体验的改进,显著优化了开发者的工作流程。即便构建Windows服务,现代泛型宿主模型也比.NET Framework中的任何方案都优越数倍。

      1. 3.5居然还在支持?它可是2007年发布的!

    3. 我记得那段岁月。但必须承认:.NET Core和.NET 5+确实卓越非凡。它们将你所说的便捷性带入了云端、Linux和容器环境。当然UI开发是个显著例外,不过自2007年3.5发布以来,该领域格局已发生五次巨变。

    4. 这种观点让我感到困惑。我从事.NET开发已有25年。我不明白为什么不能在Visual Studio/VSCode 2026中用.NET 10直接编写“框架3.5”风格的代码?我认为几乎所有3.5时代的文件都能直接导入编译,几乎无需修改。我甚至想不出有什么新语言特性是强加给开发者的。

    5. 这让我想起算法课教授的做法:他总让我们用Java实现修改过的标准算法,然后在下节课开头快速批改。他会挑出代码里奇怪的部分,瞬间替换掉,留下你原本干净有效的实现,却挂满20条弃用警告。

      说到底两种方案/语法都能运行,但…

发表回复

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

你也许感兴趣的: