news 2026/9/18 22:37:56

.NET 共享包存储(Shared Package Store)设计全解析:dotnet store 布局、发布过滤与主机探测

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET 共享包存储(Shared Package Store)设计全解析:dotnet store 布局、发布过滤与主机探测

.NET 共享包存储(Shared Package Store)设计全解析:dotnet store 布局、发布过滤与主机探测

【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime

本文以 dotnet/runtime 仓库中的官方设计文档 DotNetCore-SharedPackageStore.md 为主体,结合仓库内主机(host)层真实实现展开,系统讲解 .NET 共享程序集存储(Shared Package Store)的目录布局、dotnet store命令、基于profile.xml的发布过滤机制,以及运行时如何通过DOTNET_SHARED_STORE环境变量与探测优先级查找共享程序集。读完本文,你将掌握如何为云主机、应用托管平台搭建机器级共享包缓存、如何把应用发布为“可瘦身”的便携应用,并理解.deps.jsonruntimeconfig.json与主机探测之间的底层协作关系。

一、为什么需要共享包存储:问题与设计动机

为了让所有机器级(machine-wide)的 .NET Core 应用能够共享程序集,需要一个集中的共享程序集存储。它的核心价值有两点:

  • 应用瘦身(trimming):应用可以将共享程序集从自身发布目录中剔除,不再需要随身携带它们;
  • 集中维护:共享程序集由运行时安装包或托管平台统一安装、统一优化(如 crossgen 预编译),应用直接复用,既节省磁盘又缩短启动时间。

设计文档明确指出,这套机制由三部分组成:

  1. 一个集中式的共享程序集存储(即包存储)及其查找(lookup)机制,供应用在开发阶段部署阶段复用;
  2. 一组增强dotnetCLI 的命令,用于编排(compose)共享包存储条目,核心是dotnet store
  3. 一套发布机制,让应用在dotnet publish时按需过滤(filter)程序集,从而减少磁盘占用。

从实现角度看,这套机制的生命周期贯穿三个环节:dotnet store生成布局 → 通过DOTNET_SHARED_STORE环境变量接入运行时 → 主机(host)在解析.deps.json资产时按优先级探测共享存储目录。下面逐层展开。

二、包存储目录布局:全局安装目录与 dotnet 相对目录

包存储可以是全局系统级文件夹,也可以是dotnet.exe 相对文件夹,两者对应不同的安装与维护策略。

全局(Global)存储

  • 位置位于dotnet根目录下(Windows 上位于C:\Program Files (x86)\内),见下方布局图;
  • store/install目录下的包只允许通过平台安装器(MSI、pkg、deb、apt-get 等)安装;
  • dotnet store命令编排出来的包布局(后文详述)预期被直接解压(unzip)到store文件夹中——注意,这个解压步骤是手动操作

dotnet 根目录布局示意

- dotnet.exe - shared - netcoreapp2.0 + 2.0.0-preview2-00001 - store - install + refs + netcoreapp2.0 + netcoreapp2.1 + refs + netcoreapp2.0 + netcoreapp2.1

其中:

  • shared/netcoreapp2.0/<版本>是共享框架(Shared Framework,即Microsoft.NETCore.App)的经典目录;
  • store/install存放平台安装器安装的引用程序集(refs)与各 TFM 目录;
  • store根下(refsnetcoreapp2.0netcoreapp2.1等)则是dotnet store输出并手动解压得到的运行时共享包布局

文档特别说明:netcoreapp*文件夹内部的布局采用NuGet 缓存布局(NuGet cache layout),即{package-name}/{package-version}/{asset-path}的层级结构,这样主机在探测时可以直接复用与 NuGet 缓存一致的相对路径拼接规则。

三、使用dotnet store编排运行时共享包存储

3.1 命令的定位与目标用户

dotnet store用于编排共享包存储的布局。设计文档预期两类用户会使用它:

  • 托管提供商(hosting providers,如 Antares 云服务):用它预热(prime)服务器,提前把共享包布局部署到每台机器;
  • 框架作者(framework authors):用它制作预优化(pre-optimized)的包归档,即压缩归档布局分发给用户。

3.2 清单文件:msbuild 格式的 packages.xml

dotnet store的输入是一份包名与版本列表,以 XML 形式给出。文档强调:packages.xml必须是 msbuild 格式,因为它是接入 SDK 其余功能的入口点("it forms the entry point from which the rest of the SDK's functionality can be accessed")。

文档给出的 Roslyn 示例:

<Project Sdk="Microsoft.NET.Sdk"> <ItemGroup> <PackageReference Include="Microsoft.CodeAnalysis" Version="1.3.2" /> <PackageReference Include="Microsoft.CodeAnalysis.VisualBasic" Version="1.3.2" /> <PackageReference Include="Microsoft.CodeAnalysis.VisualBasic.Features" Version="1.3.2" /> </ItemGroup> </Project>

托管提供商会创建一份与自家托管环境中将要共享的包一一对应的packages.xml,然后交给dotnet store。该文件既可以位于文件系统上,也可以来自一个 URL(文档示例中发布命令就出现了https://asp.net/core/dev/1.2.0/profile.xml形式的远端清单)。

3.3 命令语法与参数说明

dotnet store --manifest packages.xml --framework netcoreapp2.0 [--output C:\Foo] --runtime win7-x64 --framework-version 2.0.0-preview2-00001 [--no-optimize]

各参数的完整语义(原文档参数表):

参数说明
--framework指定该包存储适用的目标框架名字(TFM),例如netcoreapp2.0;该值用于上文的共享包布局目录名
--output创建包存储的输出目录;默认值%USERPROFILE%\.dotnet(Windows)或~/.dotnet(Linux/macOS)
--skip-optimizationrestore 之后对程序集执行 crossgen(优化是默认行为)
--runtime这些程序集将要运行的目标平台运行时标识符(RID),如win7-x64
--framework-version运行这些程序集所使用的Microsoft.NETCore.App包版本,如2.0.0-preview2-00001

注意文档中命令示例使用--no-optimize,而参数表里写作--skip-optimization,二者表达的是同一开关的不同历史命名;核心语义是“优化(crossgen 预编译)默认开启,可显式跳过”。

3.4 优化流程与输出布局

当指定--optimize(即默认开启优化)时,dotnet store先在临时文件夹中把所有托管资产预编译(crossgen)为原生代码,再复制到输出目录。使用的 crossgen 工具来自--framework-version所指定Microsoft.NETCore.App的闭包(closure)内获取的那个版本——这保证了预编译结果与目标运行时版本严格匹配。

如果未指定--output,默认输出到~/.dotnet%USERPROFILE%\.dotnet\。最终输出资产文件的布局为:

$HOME/.dotnet/packages/{tfm}/{package-name}/{package-version}/{asset-path}

该输出目录通过添加到DOTNET_SHARED_STORE环境变量被运行时消费(探测优先级见后文第五节)。

四、使用共享包构建应用:从项目编写到发布过滤

4.1 核心思路:把“便携/独立”的决策推迟到发布时刻

当前构建共享程序集应用的机制是:不在项目文件中指定 RID,此时采用便携应用模型(portable app model),属于Microsoft.NETCore.App的程序集在dotnet安装根目录下查找。

引入共享包存储后,应用获得了从发布输出中过滤任意一组包的能力。因此,应用是“便携”还是“独立”(standalone)的决策,不再在项目编写(authoring)时做出,而是在发布(publish)时做出

4.2 项目编写(Project Authoring)

默认把Microsoft.NETCore.App视作始终指定了type: platform,因此用户无需显式指定 RID。相应地,在 csproj 中通过<RuntimeIdentifier/>标签指定 RID 将被视为错误(ERROR)

4.3 dotnet restore

因为 RID 要到发布时刻才可用,所以 restore 阶段应用被当作“当今的便携应用”处理,执行一次常规 restore。

4.4 dotnet build

dotnet build应把任何项目都当作便携应用处理,并生成带有Microsoft.NETCore.App框架项的runtimeconfig.json。此外,runtimeconfig.json还应包含 TFM 字段:

"runtimeOptions": { "tfm": "netcoreapp2.0", "framework": { "version": "2.0.0", "name": "Microsoft.NETCore.App" } }

文档特别对比了新旧行为差异(针对 csproj 中指定了<RuntimeIdentifier/>的应用):

  • 当前行为(Current Behavior):从 NuGet 缓存中挑取M.N.A程序集,无法利用共享Microsoft.NETCore.App提供的优化;
  • 新行为(New Behavior)M.N.A程序集取自共享框架,其余程序集取自共享包存储或 NuGet 缓存。

此外,dotnet build可以利用dotnet根目录下store/install/目录中可用的refs文件夹,从而支持离线 restore-build 场景。设计文档说明:虽然未来打算增强dotnet build对引用程序集的处理,但本设计范围内只聚焦运行时程序集(runtime assemblies)。

4.5 dotnet publish:基于 profile.xml 的过滤发布

dotnet publish将被增强为支持一个以 XML 表示的过滤配置文件(filter profile file)。该文件显式列出所有需要从发布输出中剔除的资产包。文档给出多种应用类型的发布示例:

发布便携应用:

dotnet publish

为当前 RID 发布独立应用:

dotnet publish --standalone

为 win7-x64 发布独立应用并按 profile 过滤:

dotnet publish --runtime win7-x64 filter https://asp.net/core/dev/1.2.0/profile.xml

为 win7-x64 发布便携应用并按 profile 过滤:

dotnet publish filter https://asp.net/core/dev/1.2.0/profile.xml

发布“Windows 级便携应用”(即 RID 特定的便携应用)——例如过滤掉所有非 Windows RID,但把 bin 目录按 Windows RID(如 win8、win7、win10)拆分:

dotnet publish filter https://asp.net/core/dev/1.2.0/win.profile.xml

若要串联多个 profile,只需重复指定多次filter

需要特别理解profile.xml的语义边界:它指定要从dotnet publish输出中过滤掉的精确 RID 特定或 IL 包——即物理文件会被从发布输出中剔除;但deps.json(逻辑资产清单)仍然保留profile.xml中列出的条目。这一点至关重要:它解释了为什么运行时在探测时能够“知道”某个资产本该来自共享存储——deps.json中仍然记录着该资产的逻辑信息。

五、主机探测:共享存储如何被运行时消费

5.1 探测顺序(probe precedence)

原文档指出,dotnet run以及dotnet publish之后的应用激活,都由主机按照 host-probing.md 中描述的顺序进行探测。该文档给出的按优先级排序的探测路径列表(先尝试的排前面)为:

  1. Servicing 路径(仅用于serviceable: true的资产,基础路径 Windows x64 为%ProgramFiles(x86)%\coreservicing、Windows x86 为%ProgramFiles%\coreservicing、Linux/Mac 为$CORE_SERVICING,其下分<base>/|arch|的 NI 探测路径与<base>/pkgs的常规探测路径);
  2. 应用(或框架)目录
  3. 框架目录(从高层框架向低层框架,应用视为最高层);
  4. 共享存储路径(Shared store paths)
    • $DOTNET_SHARED_STORE/|arch|/|tfm|—— 环境变量可包含多个路径,每个都会被追加|arch|/|tfm|后作为探测路径;
    • 若应用通过dotnet.exe执行,则使用 dotnet.exe 所在目录的相对路径:<dotnet.exe path>/store/|arch|/|tfm|
  5. 附加探测路径--additionalprobingpath命令行参数,以及.runtimeconfig.json/.runtimeconfig.dev.json中为应用和每个框架(从高层到低层)指定的additionalProbingPaths

文档还提示:就探测而言,框架依赖应用(framework-dependent)与自包含(self-contained)应用的主要区别在于,自包含应用没有任何框架依赖,因此所有资产(包括通常来自框架的程序集)都在应用目录中探测。

5.2 源码级实现:shared_store.cpp

仓库中的实际实现位于 src/native/corehost/hostpolicy/shared_store.cpp,完整印证了设计文档的描述:

  • 常量定义:RUNTIME_STORE_DIRECTORY_NAMEstoreSHARED_STORE_ENVDOTNET_SHARED_STORE
  • get_paths(tfm, host_mode, host_path)首先处理旧版兼容MNA < 1.1.*runtimeconfig.json不包含 TFM 属性,此时tfm为空,直接返回空列表(不探测共享存储);
  • get_env_dirs读取DOTNET_SHARED_STORE环境变量,按路径分隔符(PATH_SEPARATOR切分成多个路径,对每个路径调用pal::fullpath规范化后,依次追加archtfm目录并加入候选列表——这正对应文档中$DOTNET_SHARED_STORE/|arch|/|tfm|的规则;
  • 仅当host_mode == host_mode_t::muxer(即通过dotnet.exemuxer 启动)时,才会把dotnet.exe所在目录下的store/<arch>/<tfm>追加为候选路径——即“dotnet.exe 相对共享存储文件夹”。

5.3 源码级实现:deps_resolver.cpp 中的探测装配

src/native/corehost/hostpolicy/deps_resolver.cpp 中setup_probe_config的装配顺序与 host-probing.md 完全一致:

// Servicing NI probe / Servicing normal probe m_probes.push_back(probe_config_t::svc_ni(ext_ni)); m_probes.push_back(probe_config_t::svc(ext_pkgs)); // The published deps directory m_probes.push_back(probe_config_t::published_deps_dir()); // The framework locations, starting with highest level framework // ... m_probes.push_back(probe_config_t::fx(...)); // Shared store probes setup_shared_store_probes(shared_stores); // Additional probing paths m_probes.push_back(probe_config_t::lookup(probe));

其中setup_shared_store_probes对每个共享存储目录做了存在性检查——只有pal::directory_exists(shared)为真的目录才会被注册为lookup类型的探测路径,并置位m_needs_file_existence_checks = true(即这些目录内的命中必须做文件存在性确认)。这保证了未安装共享包的机器上,探测不会因为不存在的目录而产生额外开销。

probe_deps_entry中按m_probes顺序逐条尝试拼接资产相对路径(如newtonsoft.json/11.0.2/lib/netstandard2.0/Newtonsoft.Json.dll)并检查文件是否存在,命中即止;全部未命中则报错。特别地,deps_resolver.cpp中定义了ManifestListMessage

This assembly was expected to be in the local runtime store as the application was published using the following target manifest files: %s

即当应用发布时使用了某个目标清单(profile)进行过滤、但运行时在本地共享存储中找不到对应程序集时,主机会给出包含该 manifest 文件名的明确错误提示——这是设计文档“deps.json仍保留被过滤条目”这一语义在运行时的落地体现。

六、典型应用场景

6.1 ASP.NET

方式一:作者制作共享包安装器(eager cache,预填充缓存)

  1. 从干净目录和一份packages.xml包列表开始;
  2. dotnet store在目录中产出布局;
  3. 将该布局打包为 MSI/pkg/deb 以及 zip;
  4. 发布与安装器配套的profile.xml文件,供用户同步使用;
  5. 开发者/部署管理员把 MSI/zips 安装到部署机器上。

方式二:让应用部署者自行缓存共享包(lazy cache,懒缓存)

  1. 发布可用于执行dotnet storepackages.xml
  2. 发布可用于发布过滤的profile.xml
  3. 部署管理员在运行应用时执行dotnet store

随后,开发者/部署管理员执行dotnet publish filter profile.xml产出一个不含共享组件的 ASP.NET 应用;或者安装 MSIs/zips 后直接dotnet run

6.2 Antares(托管平台)

  • Antares 用包列表配合dotnet store在某个文件夹产出布局;
  • 将该文件夹链入环境变量DOTNET_SHARED_STORE
  • 从源码构建应用时,执行dotnet run以拾取共享包;
  • 发布运行应用时,执行dotnet publish filter profile.xml(使用 Antares profile)。

6.3 Roslyn(编译器工具链)

  • Roslyn 包位于包目录中;
  • 使用 Roslyn 的应用必须显式依赖 Roslyn,而不能通过M.N.AMicrosoft.NETCore.App)间接获得;
  • 应用可以选择使用我们发布的 Roslyn profile 文件跳过这些 DLL 的发布;
  • 应用部署到已安装运行时(且 Roslyn 包随运行时安装器一起安装)的另一台机器上即可运行。

6.4 托管预热(Hosting Primers)

场景 A:托管已发布的应用

  1. dotnet store产出布局,或解压一个既有布局;
  2. 设置DOTNET_SHARED_STORE指向该布局;
  3. 发布的性质取决于是否使用托管方 profile:
    • 使用托管 profile:应用发布目录不包含从布局中拾取的被过滤文件;
    • 不使用托管 profile:应用发布目录中的程序集覆盖布局中的同名程序集(status quo,维持现状语义)。

场景 B:从源码构建用户应用

  1. dotnet store
  2. 压缩布局(zip);
  3. 部署到托管服务器;
  4. 使用 filter 发布应用。

七、设计范围内的后续工作项(Work Items)

原文档在最后列出了两个子系统的工作项,可以作为理解该功能完整落地范围的地图:

Core-Setup(运行时/主机侧)

  • 将 dotnet 根目录下Microsoft.NETCore.App的名称改为netcoreapp2.0(需保持兼容);
  • 把 Roslyn 程序集移出共享框架(shared framework);
  • 在 Windows、Ubuntu 和 OSX 上为 Roslyn(共享)包构建安装器;
  • 实现主机对用户级与全局包存储的探测;
  • 在主机中纳入 TFM 变更的适配。

CLI(SDK 侧)

  • 让 CLI 从共享包根目录消费 Roslyn 依赖(因为其已不再属于M.N.A);
  • dotnet restoretype: platform语义进行 restore;
  • dotnet build把项目视为type: platform
  • 支持dotnet publish filter
  • 完成dotnet store的完整实现。

八、总结与最佳实践要点

结合设计文档与仓库实现,使用共享包存储的正确姿势可以归纳为四条主线:

  1. 布局即契约:无论全局目录还是 dotnet 相对目录,store下都是 NuGet 缓存布局的{tfm}/{package-name}/{package-version}/{asset-path}结构,任何手工拼装都必须遵循该结构;
  2. 清单驱动编排:用 msbuild 格式的packages.xml声明共享包集合,dotnet store负责还原、crossgen 优化并产出布局;输出目录通过DOTNET_SHARED_STORE接入运行时;
  3. 发布时决定形态:项目编写阶段不写 RID(<RuntimeIdentifier/>为错误),dotnet publish时再用filter profile.xml(可多次串联)决定剔除哪些物理文件——deps.json保留逻辑条目,运行时按 host-probing.md 的优先级顺序(servicing → 应用目录 → 框架目录 → 共享存储 → 附加路径)完成解析;
  4. 失败可诊断:若发布时依赖了共享存储、运行时却找不到程序集,主机会打印包含目标 manifest 文件名的明确错误信息(见 deps_resolver.cpp 中的ManifestListMessage),排查时应先确认DOTNET_SHARED_STORE指向的布局与发布时使用的 profile 是否一致。

这套设计本质上是把“框架共享”的思想从Microsoft.NETCore.App推广到任意一组可共享的包:共享框架解决的是运行时自带的共享,共享包存储解决的则是“平台额外预置的共享”,两者通过统一的主机探测机制无缝衔接。

【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 22:37:29

AI对话结构化导出:从文本快照到可计算数据资产

1. 项目概述&#xff1a;当对话数据不再“一坨文本”&#xff0c;而成为可计算、可追溯、可联动的结构化资产你有没有遇到过这样的场景&#xff1a;跟AI聊了半小时&#xff0c;想把关键结论整理进周报&#xff0c;结果复制粘贴时发现——对话里混着问候语、语气词、临时追问、撤…

作者头像 李华
网站建设 2026/9/18 22:34:46

ETL详解:从数据流图到增量抽取与清洗加载的实践指南

简介&#xff1a;一份面向数据仓库初学者与 ETL 开发者的 PPT 课件&#xff0c;围绕 ETL 抽取、转换、加载全流程展开&#xff0c;系统梳理 ETL 定义、实施前提、设计原则、模式比较及常见问题处置&#xff0c;既可用于个人学习&#xff0c;也可作为技术培训或面试准备的参考资…

作者头像 李华
网站建设 2026/9/18 22:34:17

Vue组件样式隔离实战:从scoped原理到CSS Modules与CSS变量方案选型

最近在重构一个 Vue 3 TypeScript 的中后台项目&#xff0c;代码量一上来&#xff0c;最先失控的不是逻辑&#xff0c;而是样式。改一个按钮的颜色&#xff0c;莫名带崩了三个页面的间距&#xff1b;给弹窗加了条边框&#xff0c;结果全局的.card全被顶了一像素&#xff1b;更…

作者头像 李华