Windows Server 2016 上要跑一套老业务系统,前置条件里写着"需要 .NET Framework 3.5",于是打开服务器管理器勾上角色和功能一路下一步,结果进度条走到一半弹出一条红字:安装一个或多个角色、角色服务或功能失败,错误代码 0x800F081F。去网上搜,答案五花八门,有人说挂 ISO 用 DISM,有人说改组策略,有人说装个 dotnetfx35.exe 就行——照着试一圈,问题还在。这个场景我做运维这些年遇到过不下几十次,Server 2016、2019、2022 甚至客户端 Windows 10/11 上的处理思路是一脉相承的。这篇就把 .NET 3.5 在 Server 2016 上装不上的来龙去脉、离线安装的完整操作、内网更新点环境的绕行方案,以及我自己在排查链路上总结的顺序,一次性讲清楚。适合正在被这台老组件卡住的中初级运维、刚接手服务器的新人,也适合想搞明白"按需功能"到底是怎么一回事的技术爱好者。
1. Server 2016 上装 .NET 3.5 为什么不像装个补丁那么简单
1.1 从系统自带组件到"按需功能"的转变
很多人的困惑点在于:明明 Server 2008 R2 时代勾一下就好了,怎么到了 2016 就变得这么麻烦。这个变化发生在 Windows 8 和 Windows Server 2012 这个时间点,微软把 .NET Framework 3.5 从"随系统镜像一起落盘的内置组件"改成了Features on Demand(按需功能)。
按需功能的含义是:系统里只保留这个组件的"占位描述",也就是 CBS(Component Based Servicing,基于组件的服务)数据库里的一条记录,告诉你"有这么个东西、它由哪些子功能组成、依赖什么";真正的二进制载荷,也就是那些 cab 包,并不随系统盘一起装到本地。当你勾选、启用它的时候,系统才会去一个"源"那里把载荷取回来,交给 CBS 去做实际的落盘和注册。
微软这么改有两个很现实的考量。一是体积,.NET 3.5 全套载荷加上各种语言包,几百 MB 打底,对于云主机镜像、批量部署的操作系统模板来说,这是白白浪费的空间,而绝大多数新应用早就跑在 4.x 上了。二是维护成本,把可选组件从主镜像里剥离出去,主镜像的更新和安全补丁链路会清爽很多。
理解了这一点,很多现象就顺理成章了:为什么断网的服务器一定装不上、为什么指向一个路径不对的目录会报"找不到源文件"、为什么同样的命令在这台机器上能用在那台上不能用。因为你的操作本质上不是"安装一个软件",而是"让 CBS 从一个源里取回一个已登记的组件载荷"——源有问题,一切免谈。
注意:.NET 3.5 和 .NET 4.x 是两条完全独立的运行时,装 4.8 不会顺带把 3.5 带上,反过来也一样。老应用报"需要 3.5",指的是 v2.0 CLR 那一套运行时(.NET 3.5 内部仍然使用 CLR 2.0 引擎),和 4.x 的 CLR 4.0 互不替代。
1.2 三个高频错误码分别指向什么
在动手之前,先看懂错误码比急着敲命令重要得多。.NET 3.5 安装失败常见的错误码就那么几个,但它们背后的病因完全不是一回事,用错药只会浪费半天时间。
- 0x800F081F:找不到源文件。这是出现频率最高的一条。典型场景是服务器没有外网,系统按默认逻辑试图去取载荷却取不到;或者你指定了
/Source路径,但那个路径里没有对应的 cab 包;又或者是介质版本、语言和当前系统对不上。它表达的是"我要的东西在指定的地方没找到"。 - 0x800F0906:无法下载源文件。系统能确定该去哪拿,但在"拿"这个动作上失败了——更新服务没起来、网络出口不通、DNS 解析异常、某些安全软件拦了出站请求。它和 081F 的区别在于:一个是没找到地方,一个是找了但拿不回来。
- 0x800F0907:被策略挡住。服务器的更新行为被组策略或注册表约束成"只能从某个内部更新点获取"或者"从不检查更新",系统根本不允许主动去外部取载荷。
- 0x800F0954:这个码在内网机器上特别常见。系统试图直接访问外部更新源,但被"内部统一更新点"的策略拦截,于是返回失败。它的潜台词是"策略不允许我绕过内部更新点,可内部更新点里又没有这个载荷"。
- 0x800F0922:相对模糊的一条,系统盘空间不足、BITS 服务异常、组件存储有轻微损坏都可能触发。
记住这个判断逻辑:先看错误码分大类,再往对应的方向查,比盲目试方案效率高得多。我见过有人在 0x800F0954 的情况下反复重挂 ISO,其实策略根本没放开,挂一百次也没用。
1.3 动手前的两项确认:装没装、系统是什么版本
正式动手前一定要做两件事,这两件事加起来不超过三分钟,但能省掉后面大量的无效操作。
第一,确认目标功能是不是真的没启用。有些老软件的检测逻辑很粗暴,它可能只是判断注册表某个键不存在就报"缺少 .NET 3.5"。用下面任意一条命令确认实际状态:
# DISM 方式 dism /online /get-featureinfo /featurename:NetFx3# PowerShell 方式(Server 专用模块) Get-WindowsFeature NET-Framework-Core也可以直接查注册表:
reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5" /v Install返回Install REG_DWORD 0x1就说明运行时本体已经在了。如果确认已经在,那问题就不在安装环节,得往应用自身的配置上找。
第二,确认系统的准确版本号。Server 2016 对应的内核版本是10.0.14393,这个数字非常关键,因为 sxs 载荷的来源介质必须和它对齐。用winver或者下面这条命令看一眼:
[System.Environment]::OSVersion.Version别小看这一步。我遇到过一次,运维同事从文件服务器上随手拿了一个 Server 2019 的 ISO,对着 14393 的系统指 sxs 路径,报的就是 0x800F081F。跨版本的内核组件编号对不上,CBS 的适用性检查直接不通过,报错信息还特别含糊,很容易误导人往路径写错的方向查。
2. 离线安装全流程:从安装介质里取 sxs 载荷
2.1 挑一份能用的安装介质
离线安装的核心思路就一句话:把安装介质里sources\sxs这个目录当作载荷源,指给 DISM 或 PowerShell。所以第一步是准备一份靠谱的介质。
选择标准按重要性排序:
- 版本必须匹配,也就是 Server 2016,内核 14393。评估版(Eval)镜像在文件层面和正式版是同一套组件,通常也能用,但生产环境我还是建议用和系统一致的正式介质,少一层不确定性。
- 语言尽量匹配。中文系统的服务器优先用中文介质。语言的匹配度会影响某些语言相关子功能的载荷,虽然多数情况下 NetFx3 的主包能跨语言装,但一旦涉及子功能就会出问题,不如一开始就对齐。
- 必须是完整镜像。这一点我放在 5.1 里单独展开,是踩过坑的。
介质到手后,在服务器上挂载它(虚拟光驱或者直接右键装载 ISO),记下分配的盘符,假设是E:。
2.2 挂载、校验、落地到本地磁盘
挂载之后,别急着敲安装命令,先做一次校验——看看源里到底有没有你要的东西。
# 列出 sxs 目录里和 netfx3 相关的载荷文件 Get-ChildItem E:\sources\sxs -Filter *.cab | Where-Object { $_.Name -like "*netfx3*" } | Select-Object Name, Length正常情况下你应该能看到类似microsoft-windows-netfx3-ondemand-package.cab这样的文件名,加上若干带语言标记的包。如果这条命令什么都没输出,先别往下走,问题出在介质本身,换一份再来。
确认有载荷之后,强烈建议把整个 sxs 目录复制到本地磁盘,而不是直接用 ISO 里的路径:
# 复制到本地,路径随意,别带空格和中文 Copy-Item E:\sources\sxs -Destination C:\sxs -Recurse为什么要多这一步?三个原因。第一,ISO 的挂载是临时的,服务器一重启挂载点就没了,如果安装过程因为某些原因中断需要重试,你会发现源不见了,报错又变成 0x800F081F,很容易被带偏。第二,挂载分配的盘符不是固定的,虚拟机里多挂几个盘就可能漂移。第三,从本地磁盘读取的稳定性明显更好,组件安装过程涉及大量小块文件的读取,从物理磁盘读比从虚拟光驱读更可控。
复制完成后确认一下目标目录里的文件数量对不对:
(Get-ChildItem C:\sxs -Filter *.cab).Count正常的完整 sxs 目录里 cab 文件是成百上千个的,如果只有个位数,说明源介质本身就有问题。
2.3 DISM 与 PowerShell 两条命令路径怎么选
源准备好之后,安装命令其实就一条,但 DISM 和 PowerShell 两种写法各有适用场景。
DISM 写法(最通用,Server Core 和带 GUI 的版本都能跑):
dism /online /enable-feature /featurename:NetFx3 /all /source:C:\sxs /limitaccess /norestart参数逐个解释一下,这几个参数每一个都有它的作用,少一个都可能出问题:
| 参数 | 作用 | 为什么必须加 |
|---|---|---|
/online | 针对当前运行的系统操作 | 不加会去操作离线映像,方向就错了 |
/enable-feature | 启用功能 | 与/disable-feature相对 |
/featurename:NetFx3 | 指定 .NET 3.5 的功能名 | 名称必须准确,写错直接报不支持 |
/all | 连同所有父功能和子功能一起启用 | 不加的话 WCF、HTTP Activation 这些子功能不会装,很多老应用恰恰依赖它们 |
/source:C:\sxs | 指定载荷来源目录 | 关键参数,指向的是目录本身,不是里面的 cab 文件 |
/limitaccess | 禁止联系外部更新源 | 最关键的一个,不加它 DISM 可能仍会尝试联网,内网机器就会卡很久然后失败 |
/norestart | 安装完不自动重启 | 生产环境控制重启时机,建议加上 |
PowerShell 写法(Server 2016 上更顺手,能拿到结构化输出):
Install-WindowsFeature -Name NET-Framework-Features -Source C:\sxs -IncludeAllSubFeature或者用 DISM 模块的 cmdlet:
Enable-WindowsOptionalFeature -Online -FeatureName NetFx3 -All -Source C:\sxs -LimitAccess -NoRestart两条 PowerShell 路径的差别:Install-WindowsFeature来自 ServerManager 模块,是服务器角色的管理入口,它的-IncludeAllSubFeature对应 DISM 的/all;Enable-WindowsOptionalFeature来自 Dism 模块,更贴近底层的可选功能语义。功能上都够用,我个人的习惯是服务器角色相关的统一用Install-WindowsFeature,因为输出格式和服务器管理器里的显示是一致的,排查时对得上。
跑完之后,正常情况下会看到进度提示然后返回成功。如果返回的是The operation completed successfully或者 PowerShell 里的Success: True,说明 CBS 已经接受了这个安装请求。
2.4 装完怎么验证才算真的成功
命令返回成功不等于万事大吉,CBS 的安装是异步推进的,而且有"待重启"状态。验证分三步走。
第一步,看功能状态:
Get-WindowsFeature NET-Framework-*输出里NET-Framework-Core、NET-Framework-45-Core、NET-Framework-Features这些条目的Install State都应该是Installed。注意 Core 和 Features 是两个层级,Core 是运行时本体,Features 是包含子功能的合集。
第二步,看子功能有没有跟上:
dism /online /get-featureinfo /featurename:NetFx3输出里的State : Enabled是基本要求,下面还会列出父功能和各子功能的启用状态。如果子功能里有Disabled,说明/all没生效或者载荷不全,需要补装。
第三步,回归注册表:
reg query "HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5" /v Install返回0x1就实锤了。
三步都通过之后,重启一次服务器。这一步不是可选的,CBS 有些注册和文件替换操作在待重启状态下不会完全提交,不重启的话某些老应用仍然会报找不到运行时。重启后再跑一遍第一步的验证,状态稳定在 Installed,这个环节才算真正收尾。
到这里,离线安装的主线就走完了。如果一切顺利,这大概是十分钟的工作量。真正耗时间的是失败之后的排查,那才是这篇文章的重点部分。
3. 联网机器与内网机器的两条岔路
3.1 允许直连更新源时需要满足什么
如果你的服务器能正常访问外部更新源,其实不用折腾 ISO 和 sxs,直接在服务器管理器里勾选 .NET Framework 3.5 让它自己去取就行。但"能联网"和"更新链路是通的"是两件不同的事,我见过太多"能 ping 通但装不上"的案例。
按这个顺序检查:
- Windows Update 服务状态。
wuauserv必须能正常启动。有些做过"优化"的镜像或者被第三方工具处理过的系统,这个服务被设成了禁用,重启后也不会自己起来。 - BITS 服务状态。后台智能传输服务,负责实际的数据下载,被禁用的话下载动作根本发起不了。这两条可以一起看:
Get-Service wuauserv, BITS | Select-Object Name, Status, StartType- 服务启动类型。光看状态是 Running 不够,还要确认
StartType是Automatic或Manual,如果显示Disabled,先改回来:
Set-Service -Name wuauserv -StartupType Manual Set-Service -Name BITS -StartupType Manual- 网络出口配置。如果内网是统一出口上网,系统层面的 HTTP 出口设置要确认一下:
netsh winhttp show proxy如果这里配置了出口而实际环境里并不需要,或者配置的地址已经失效,下载同样会失败。需要清掉的话用netsh winhttp reset proxy。
DNS 解析与时间同步。听起来很基础,但系统时间偏差过大导致的证书校验失败,报出来的错误码也常常是下载类错误,方向很容易带偏。
安全软件的拦截。某些终端防护产品会对组件安装过程中的临时文件写入做拦截,表现就是安装走到一半失败。这个只能通过临时观察或者查软件日志来确认。
3.2 组策略里"指定可选组件安装和组件修复的设置"怎么配
这个策略项是所有 .NET 3.5 安装问题的关键开关,路径在:
计算机配置 → 策略 → 管理模板 → 系统 → 指定可选组件安装和组件修复的设置
英文环境下叫Specify settings for optional component installation and component repair。它的三种状态对应三种完全不同的行为:
| 策略状态 | 系统行为 | 适用场景 |
|---|---|---|
| 未配置 | 默认允许直接从外部更新源获取载荷 | 能上网的单机,装了就能成功 |
| 已启用 + 勾选"联系 Windows 更新以直接下载修复内容" | 允许直连更新源获取,同时保留内部更新点回退 | 能上网但也有内部更新点的环境 |
| 已启用 + 不勾选 | 只允许从内部统一更新点获取,禁止直连 | 严格隔离的内网 |
看清楚这张表,前面那些错误码就都能对上了。0x800F0907 基本就是策略被设成了"只走内部更新点",但内部更新点里没有这个载荷;0x800F0954 则是策略把直连请求给拦了。
修改策略之后一定要让策略立即生效:
gpupdate /force然后确认实际生效的值,别只改了本地组策略却发现域策略优先覆盖了:
gpresult /h C:\gpo-report.html生成的报告用浏览器打开,能清楚看到最终生效的是哪条策略、来自哪个层级。域环境下这一条特别重要,本地改了半天被域策略覆盖是常有的事。
提示:改策略属于环境级变更,动手前先把原值记下来(截图或者导出报告都行)。装完 .NET 3.5 之后把策略改回去,避免影响后续其他组件的安装行为。
3.3 内网统一更新点环境下的 0x800F0954 绕行
域内的服务器十有八九套了统一的更新策略,所有更新行为都指向内部的更新服务器。这种环境下安装 .NET 3.5,最省事的做法根本不是去和政策较劲,而是直接用本地 sxs 源,配合/LimitAccess完全绕过更新链路。
回到第 2 章的那条命令:
dism /online /enable-feature /featurename:NetFx3 /all /source:C:\sxs /limitaccess /norestart/limitaccess的作用就在这里体现——它明确告诉 DISM:不要尝试联系任何更新源,就用我给你的这个目录。这样无论内网策略怎么配,都不会触发下载动作,也就不会撞上 0x800F0954。
如果出于各种原因必须走内部更新点这条路,那要检查的是内部更新服务本身有没有同步对应的产品分类和更新类型。这部分需要管理更新服务器的人配合,不是单机能解决的。我的建议是:为了装一个 NetFx3 去改整条更新链路的配置,风险和收益完全不成比例,本地源方案又稳又快,没有理由不用。
顺带说一句,同一套逻辑在客户端 Windows 10/11 上也成立,只是功能名和命令入口略有差别,客户端用的是dism /online /enable-feature /featurename:NetFx3或者Enable-WindowsOptionalFeature,源同样指向对应版本镜像的 sxs 目录。思路是打通的。
4. 装不上时的排查链路:从错误码到 CBS 日志
4.1 一张错误码对照表先把范围缩小
排查这件事最怕的是没有章法地乱试。我一般会先把错误码对着表看一遍,把可能性从十几个收敛到两三个,再去逐个验证。下面这张表是我自己整理并一直在用的,实测覆盖了绝大多数场景。
| 错误码 | 字面含义 | 最可能的根因 | 优先动作 |
|---|---|---|---|
| 0x800F081F | 找不到源文件 | 路径写错、介质版本/语言不匹配、sxs 目录被精简、未加 /LimitAccess | 核对路径内容与介质版本 |
| 0x800F0906 | 无法下载源文件 | 更新服务异常、网络出口不通、DNS 或时间偏差 | 查服务状态与出口配置 |
| 0x800F0907 | 被策略阻止 | 策略设为"只从内部更新点获取"或"从不检查更新" | 查组策略生效报告 |
| 0x800F0954 | 内部更新点拦截 | 统一的更新策略阻止了直连请求 | 改用本地源加 /LimitAccess |
| 0x800F0922 | 一般性失败 | 系统盘空间不足、BITS 异常 | 查磁盘剩余空间与服务 |
| 0x80073712 | 组件存储损坏 | WinSxS 组件库有损坏 | 先做组件存储修复 |
最后一条要单独说一下,因为处理方式和前面几条完全不同。如果怀疑组件存储本身有问题,需要先用同版本介质做一次修复:
dism /online /cleanup-image /restorehealth /source:C:\sxs /limitaccess注意这里的/source指向的是包含组件修复载荷的目录,用的是同样的介质,只是命令动作变成了"修复"而不是"启用功能"。修复完成后再回头装 NetFx3。顺序错了的话,安装过程会在一个已经损坏的组件库上做操作,报什么错都可能有。
4.2 CBS.log 和 DISM.log 里真正该看的那几行
错误码给的是方向,真正的原因往往藏在日志里。组件安装的主日志在C:\Windows\Logs\CBS\CBS.log,DISM 自己的日志在C:\Windows\Logs\DISM\dism.log。
CBS.log 可能非常大,几百 MB 是常事,直接打开会卡死编辑器。用 Select-String 精准过滤:
Select-String -Path C:\Windows\Logs\CBS\CBS.log -Pattern "NetFx3" | Select-Object -Last 60 | ForEach-Object { $_.Line }我关注的关键词有这么几类:
Failed to find payload或者CBS_E_SOURCE_MISSING:源里确实没有对应的载荷文件,回到 2.2 重新检查 sxs 目录内容。Not applicable或者适用性检查相关的行:版本或语言不匹配,换介质。Failed to download或下载相关的行:网络链路问题,对照 3.1 检查。Blocked by policy或策略相关的行:策略拦截,对照 3.2、3.3 处理。ERROR_SXS_...开头的:组件库层面的问题,考虑上面的组件存储修复。
DISM.log 里则主要看命令行参数是怎么被解析的,有时候问题就出在参数写错,比如/source指向了 cab 文件而不是目录,或者路径里的空格没处理好。日志里会明确记下"解析到的源路径是什么",对不上就是参数问题。
经验:如果 CBS.log 太大不方便处理,可以先复制一份出来再筛选,避免在实时写入的日志上做长时间读取影响正在进行的安装操作。
4.3 服务状态与残留策略这两个隐形杀手
有两类问题不会体现在错误码里,但会让你反复装不上,我把它们叫隐形杀手。
第一类是服务被改成禁用。除了前面提到的wuauserv和BITS,还有一个容易被忽略的是Windows Modules Installer(TrustedInstaller),它是 CBS 的执行主体。这个服务如果起不来,安装动作根本推进不了:
Get-Service TrustedInstaller, wuauserv, BITS, CryptSvc | Select-Object Name, Status, StartType四个服务的StartType都不应该是Disabled。CryptSvc(加密服务)也列进来是因为组件包的签名校验依赖它Quad。有第三方"优化"工具会一次性把这几个服务全关掉,恢复的时候记得一起恢复。
第二类是残留的策略或注册表项。有些时候组策略已经改回去了,但注册表里还留着历史配置。检查这两个位置:
HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU如果里面的配置和当前实际环境不符(比如指向了一台已经不存在的更新服务器),就是典型的残留。清理前记得先导出备份:
reg export "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" C:\wu-backup.reg另外还有一个位置值得看一眼,就是第 3.2 节讲的那个策略在注册表里的落点,通常在HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate下的相关值里。用组策略界面改是最稳妥的方式,直接改注册表容易漏掉关联项。
排查到这里,99% 的场景都能定位到了。剩下的 1% 通常是环境本身的特殊性问题,比如某些定制化的安全加固基线对系统目录写入做了额外限制,那就需要结合具体环境看了。
5. 几个反复踩过的坑和收尾动作
5.1 精简镜像的 sxs 目录可能是空的
这个坑我踩过两次,而且两次都卡了很久,因为报错信息指向的方向是错的。
现象是这样的:从内部文件服务器上拿了一份"部署专用镜像",挂载后用命令装 NetFx3,报 0x800F081F。第一反应是路径写错了,反复检查路径、盘符、权限,都没问题。后来才想起来看一眼 sxs 目录里到底有没有东西:
$sxs = "E:\sources\sxs" Get-ChildItem $sxs -ErrorAction SilentlyContinue | Measure-Object | Select-Object Count Get-ChildItem $sxs -Filter *.cab -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum | Select-Object Count, @{n='TotalMB';e={[math]::Round($_.Sum/1MB,1)}}完整的 sxs 目录,cab 文件数量是四位数的,总大小在几百 MB。如果返回的 Count 是个位数甚至 0,或者目录根本不存在,那就是镜像被人为精简过了——为了保证镜像体积,sources\sxs被整个删掉了。
这种情况下的判断依据很明确:目录大小和文件数量。养成一个习惯,挂载介质后的第一件事就是确认 sxs 目录的存在性和体积,比后面调试半天要高效得多。
顺带说,这也是为什么我建议在公司内部维护一份"完整版介质"的副本,哪怕平时用的是精简镜像。遇到需要安装按需功能的场景,直接取完整版就行,不用再去外部找。
5.2 网络路径、盘符漂移与访问上下文
技术上,/Source参数是可以接受 UNC 网络路径的,比如\\fileserver\share\sxs。但我不推荐在生产环境这么干,原因在于访问上下文。
组件安装的实际执行者是 TrustedInstaller 这个系统上下文,它访问网络共享用的是机器账户或者系统账户的身份,而不是你当前登录的管理员身份。这带来两个后果:一是权限可能不够,共享和 NTFS 权限如果只给了你的用户账户,机器账户就被拒;二是即使权限配好了,跨网络的读取在组件安装这种高频率小文件读取的场景下,稳定性和速度都不理想,偶尔会出现读取超时导致安装中断。
如果非要用网络路径,把共享权限同时授予目标服务器的机器账户,并且确认网络链路稳定。但更省事的做法还是复制到本地磁盘,几百 MB 的文件,局域网拷贝也就几十秒的事。
盘符漂移是另一个坑。虚拟机环境里,如果 ISO 是通过虚拟光驱挂载的,重启之后挂载可能失效,或者盘符变成了别的字母。所以我在 2.2 里强调把 sxs 复制到本地磁盘并且用固定路径,就是为了规避这一类问题。路径里也尽量别带空格和中文,虽然大多数情况下能处理,但偶尔会在参数解析上出幺蛾子,没必要给自己找麻烦。
5.3 组件装上了但业务系统仍然报错的情况
最后这一部分是我觉得最有价值的经验——装完 .NET 3.5 之后业务还是起不来,这种情况发生的频率比想象中高。
先排查最简单的:重启了吗?CBS 在待重启状态下有些文件操作没有完全提交,老应用加载运行时时会失败。重启一次再看。
如果重启后还不行,往下看几个方向:
一是子功能没装全。/all参数虽然会带上子功能,但如果源里的载荷不全(比如用了不完整的介质),部分子功能会静默跳过。用dism /online /get-featureinfo /featurename:NetFx3看下面列出的子功能状态,重点看 WCF 相关的几个:WCF-HTTP-Activation、WCF-NonHTTP-Activation、WCF-HTTP-Activation45之类。很多老应用尤其是服务端程序依赖 WCF 的 HTTP 激活,缺了它表现就是"运行时找不到"。
二是 IIS 场景下的配套功能。如果老应用部署在 IIS 上跑 ASP.NET 2.0 应用池,光装 .NET 3.5 本体不够,还要在 Web 服务器角色的"应用程序开发"分类下勾选:
- ASP.NET 3.5
- .NET Extensibility 3.5
- ISAPI Extensions
- ISAPI Filters
同一个分类里还有 4.x 系列的对应项,别勾错了。勾选完还要确认站点的应用池运行时版本选的是.NET CLR Version v2.0,如果应用池跑在 v4.0 上,2.0 编译的程序集加载会失败。
三是 32 位与 64 位的问题。有些老应用是 32 位的,对应的应用池需要开启"启用 32 位应用程序"选项。这个设置和应用池的位数是分开的,很容易被忽略。
四是版本混淆。再强调一次,.NET 3.5 和 .NET 4.x 是完全独立的两套运行时。应用报"需要 3.5",检查的方向就是 v3.5 注册表键和C:\Windows\Microsoft.NET\Framework\v3.5目录;如果它报的是 4.x 的问题,那和这次安装没关系,别在同一件事上钻牛角尖。
我个人这几年处理下来,最省心的做法是在公司内部维护一份标准化的"组件源包"——就是把同版本完整介质里的 sxs 目录抽出来,单独放在内部文件服务器上,配套一份操作说明和命令模板。新服务器要装 .NET 3.5,直接从内部源复制 sxs 到本地,跑一条命令,重启,验证,全程十分钟以内。域策略、内网更新链路、外网连通性这些变量全部不参与,出了问题也容易定位。这个思路在 Server 2016 之后的几个版本上一直适用,值得花点时间建起来。
另外提醒一句,服务器做完组件变更之后,记得在变更记录里写清楚装了什么、源来自哪里、是否重启过。下次同一个机房里另一台机器出同样的问题,这份记录能让你少走一大半弯路。