dotnet 9 通过 AppHostRelativeDotNet 指定自定义的运行时路径
进行框架依赖发布的时候,应用程序需要有 dotnet runtime 运行时才能跑起来。在 dotnet 9 之前,通常都是需要安装到系统的 Program File 文件夹下的全局 dotnet 运行时的支持。在 dotnet 9 时,引入了 AppHostRelativeDotNet 机制,允许开发者自定义依赖框架发布的应用使用的 dotnet 运行时路径
在 2022 时,我写了一个提案,允许应用程序自定义使用的 dotnet 运行时文件夹路径。详细请看 https://github.com/dotnet/runtime/issues/64430
这个提案的背景是我有很多个应用准备发布给到用户端上,如果这么多应用都走独立发布,自然会让用户的 C 盘充满重复的文件。如果是将 dotnet 运行时交给的是 Program File 文件夹下的全局文件夹,则可能会遇到各种被投毒问题,比如某次系统更新之后,应用程序就因为 .NET 环境损害而无法启动
我所在的团队那会也在迁移一个大型的 .NET Framework 项目到 .NET 6 上,原本的项目会有多 exe 入口问题,这部分设计也改不动。多入口情况下也不适合每个入口都做独立发布,尽管独立发布的重复 BCL 等文件能够在安装包里面被压缩,但是在安装到用户设备上时,解压缩出来的内容依然会撑满用户的 C 盘
为此,我所在的团队就制作和开源了 https://github.com/dotnet-campus/dotnetCampus.AppHost 项目,细节原理请参阅 如何让 .NET 程序脱离系统安装的 .NET 运行时独立运行?除了 Self-Contained 之外还有更好方法!谈 dotnetCampus.AppHost 的工作原理 - walterlv
关于我所在的团队迁移大型项目的经验请参阅 记将一个大型客户端应用项目迁移到 dotnet 6 的经验和决策
我那会预期的情况是这样的,我在自己控制的路径下,如 C:\Program Files\CompanyName 文件夹下,放入了自己的 DotNETRuntime[Version] 文件夹。然后再依次部署上多个应用程序,这些应用程序都是采用依赖框架(Publish framework-dependent)方式发布,总的文件夹布局情况如下
C:\Program Files\CompanyName\DotNETRuntime[Version]\
C:\Program Files\CompanyName\Produce1\
C:\Program Files\CompanyName\Produce2\
C:\Program Files\CompanyName\Produce3\
如此即可让 Produce1 Produce2 Produce3 三个产品共用一个 dotnet 运行时
我的这个提案被 dotnet 官方采纳了,加入到 .NET Host 提升计划里面,详细请看 https://github.com/dotnet/runtime/issues/97931 。经过了三年(实际上绝大部分时间都在讨论)的开发,终于在 dotnet 9 支持了这个功能,能够完全实现我预期的功能
此项功能被命名为 Embedded install location options for apphost ,被我翻译为嵌入 dotnet 安装路径到 AppHost 里的功能,我也对外宣称这是为依赖框架的应用自定义 .NET Runtime 文件夹路径的功能
接下来我将和大家介绍此功能的用法和效果
此功能涉及到的关键属性分别如下:
- AppHostDotNetSearch : 决定从哪里开始寻找,可选参数为 AppLocal、 AppRelative、 EnvironmentVariables 和 Global,可认为在此之前就是 Global 的值。允许设置多个参数,多个参数之间依然用
;分号隔开。在本文里面,核心功能将由AppRelative参数实现 - AppHostRelativeDotNet : 配置相对于 exe 的路径,这个路径将被作为 dotnet 运行时的查找路径
默认情况下,可只需设置 AppHostRelativeDotNet 属性即可。当 AppHostRelativeDotNet 属性被设置的时候,隐式设置了 AppHostDotNetSearch 属性为 AppRelative 的值。但通常来讲,可以将 AppHostDotNetSearch 属性设置为 AppHostDotNetSearch=AppRelative;Global 的值,这就意味着如果从相对路径没有找到 dotnet 运行时,将自动回滚到从 Global 全局进行查找。这里的 Global 全局即 C:\Program Files\dotnet\ 或 C:\Program Files (x86)\dotnet\ 文件夹
为了演示此功能的用法,我创建了一个名为 LinerewheldeholearjearHalllurlecayawfea 的控制台项目,控制台项目使用的是 .NET 9 默认控制台模版代码。编辑 csproj 项目文件,添加 AppHostDotNetSearch 和 AppHostRelativeDotNet 属性,修改之后的 csproj 项目文件代码大概如下。本文内容里面只给出关键代码片段,如需要全部的项目文件,可到本文末尾找到本文所有代码的下载方法
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AppHostDotNetSearch>AppRelative;Global;</AppHostDotNetSearch>
<AppHostRelativeDotNet>../relative/path/to/runtime</AppHostRelativeDotNet>
</PropertyGroup>
</Project>
可以看到我在 AppHostRelativeDotNet 写入的是相对于 exe 的上一层文件夹空间的 relative/path/to/runtime 路径
准备工作就此完成,接下来就是设置进行框架依赖发布。这里需要特别说明的是 .NET Core (包含 .NET 5 和更高版本)的输出 exe 是不能实现 .NET Framework 的 AnyCpu 魔法的,在使用自定义 dotnet 运行时路径时,需要根据自己的需求,明确指定其版本。这里也需要额外说明的是,尽管本文内容都在 Windows 下测试,但事实上本文介绍的 dotnet 这项新功能是可以在全平台使用的,即在 Linux 或 mac 上也适用

我的发布配置文件 FolderProfile.pubxml 代码如下
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net9.0\publish\win-x86\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>false</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
</PropertyGroup>
</Project>
配置完成之后,直接进行发布,此时可以看到发布创建的文件只有几个。有了这项技术就不怕发布大量工具了,有了这项技术就可以让发布的 .NET Core(包含.NET 5及更高版本)应用也和 .NET Framework 应用一样小体积占用

发布完成之后,可不能和进行独立发布(Self-Contained)一样,直接就将此分发给到用户了,咱还需要对此进行包装文件夹布局
刚才在 AppHostRelativeDotNet 写的是相对于 ../relative/path/to/runtime 文件夹,嗯,这里只能写相对文件夹路径,不能写绝对文件夹路径。那咱就需要将发布输出的文件包装为里一层文件夹,我这里选择将其放入到名为 App1 的文件夹里面,这样我如果有第二个应用,就可以放入到 App2 文件夹里面
再接着将 App1 文件夹放入到名为 App 的文件夹里面。再在 App 文件夹里面的 relative/path/to/runtime 文件夹里面放入 dotnet 运行时。如此就完成了包装文件夹布局,此时直接双击 App\App1\LinerewheldeholearjearHalllurlecayawfea.exe 就能运行了
包装完成的文件夹布局情况如下

C:\LINDEXI\APP
|
+---App1
| LinerewheldeholearjearHalllurlecayawfea.deps.json
| LinerewheldeholearjearHalllurlecayawfea.dll
| LinerewheldeholearjearHalllurlecayawfea.exe
| LinerewheldeholearjearHalllurlecayawfea.pdb
| LinerewheldeholearjearHalllurlecayawfea.runtimeconfig.json
|
\---relative
\---path
\---to
\---runtime
| dotnet.exe
| LICENSE.txt
| ThirdPartyNotices.txt
|
+---host
| \---fxr
| \---9.0.4
| hostfxr.dll
|
\---shared
\---Microsoft.NETCore.App
\---9.0.4
.version
clretwrc.dll
clrgc.dll
clrjit.dll
coreclr.dll
createdump.exe
hostpolicy.dll
Microsoft.CSharp.dll
Microsoft.DiaSymReader.Native.x86.dll
Microsoft.NETCore.App.deps.json
Microsoft.NETCore.App.runtimeconfig.json
Microsoft.VisualBasic.Core.dll
Microsoft.VisualBasic.dll
Microsoft.Win32.Primitives.dll
Microsoft.Win32.Registry.dll
...
System.Xml.XPath.dll
System.Xml.XPath.XDocument.dll
WindowsBase.dll
也许伙伴们有一个问题,那就是这里的 .NET Runtime 运行时文件夹组织是哪里来的,文件是从哪里来的。这是从 dotnet 官方下载的,下载链接是: https://dotnet.microsoft.com/zh-cn/download/dotnet/9.0
下载右边“运行应用 - 运行时”这一列的内容

运行时这一列有很多选项,具体应该下哪一个呢?这就看自己的需求了。如我只是一个简单的控制台,且准备发布的是 x86 应用,那我就应该下载 x86 二进制文件,就是这样的对应关系,先取决于要用什么框架,再决定用什么平台

下载下来的是一个 zip 压缩包,打开压缩包就可以看到这就是上文提到的 relative/path/to/runtime 文件夹内的结构,按照本文提供的方式将其解压缩就好了

额外需要说明的是,现在对于桌面应用来说是没有提供二进制包,只有安装包。即对于 WPF 和 WinForms 来说,现在只有安装包可用。那咋办呢?很简单,只需要找一个干净的系统(如虚拟机内),下载安装包且安装。安装完成之后,即可在 C:\Program Files\dotnet\ 或 C:\Program Files (x86)\dotnet\ 文件夹内找到安装输出的文件,将其拷贝出来放入到 relative/path/to/runtime 文件夹内即可
通过此项技术,即可让多个应用共用一个私有分发的 .NET 运行时。也可以作为单应用多 exe 入口程序的共享运行时技术实现。这项技术对于小工具项目特别友好,避免小工具项目要么各自带着运行时独立发布,要么被第三方或系统投毒运行时的选择
既然这可以使用私有分发的 .NET 运行时,那对于一些动手能力强的开发者来说,也可以在这里面带上自己魔改之后的 .NET 版本,实现更多有趣的功能
本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin 1e09f77a553d664872cb12324a118649ffd23ad9
以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码
git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin 1e09f77a553d664872cb12324a118649ffd23ad9
获取代码之后,进入 Workbench/LinerewheldeholearjearHalllurlecayawfea 文件夹,即可获取到源代码
参考文档:
- https://github.com/dotnet/runtime/issues/97931
- https://github.com/dotnet/designs/blob/main/proposed/apphost-embed-install-location.md
- https://learn.microsoft.com/zh-cn/dotnet/core/project-sdk/msbuild-props
- https://github.com/dotnet/runtime/issues/64430
- https://github.com/dotnet/designs/blob/main/accepted/2020/install-locations.md
更多技术博客,请参阅 博客导航
dotnet 9 通过 AppHostRelativeDotNet 指定自定义的运行时路径的更多相关文章
- C# 获取程序运行时路径
Ø 前言 开发中,很多时候都需要获取程序运行时路径,比如:反射.文件操作等..NET Framework 已经封装了这些功能,可以很方便的使用. C# 中有很多类都可以获取程序运行时路径,我们没必要 ...
- 【转】Java Web 项目获取运行时路径 classpath
Java Web 项目获取运行时路径 classpath 假设资源文件放在maven工程的 src/main/resources 资源文件夹下,源码文件放在 src/main/java/下, 那么ja ...
- Java Web 项目获取运行时路径 classpath
假设资源文件放在maven工程的 src/main/resources 资源文件夹下,源码文件放在 src/main/java/下, 那么java文件夹和resources文件夹在运行时就是class ...
- Java 9 揭秘(7. 创建自定义运行时映像)
Tips 做一个终身学习的人. 在第一章节中,主要介绍以下内容: 什么是自定义运行时映像和JIMAGE格式 如何使用jlink工具创建自定义的运行时映像 如何指定命令名称来运行存储在自定义映像中的应用 ...
- 自定义注解之运行时注解(RetentionPolicy.RUNTIME)
对注解概念不了解的可以先看这个:Java注解基础概念总结 前面有提到注解按生命周期来划分可分为3类: 1.RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成clas ...
- 自定义实现InputFormat、OutputFormat、输出到多个文件目录中去、hadoop1.x api写单词计数的例子、运行时接收命令行参数,代码例子
一:自定义实现InputFormat *数据源来自于内存 *1.InputFormat是用于处理各种数据源的,下面是实现InputFormat,数据源是来自于内存. *1.1 在程序的job.setI ...
- Visual Studio 中指定自定义生成事件
自定义生成事件打开方式 通过指定自定义生成事件,可以在生成开始之前或在它完成之后自动运行命令.在Visual Studio中通过右键项目->属性 进入项目属性菜单. 自定义生成事件的语法 生成事 ...
- 自定义 ThreadPoolExecutor 处理线程运行时异常
自定义 ThreadPoolExecutor 处理线程运行时异常 最近看完了ElasticSearch线程池模块的源码,感触颇深,然后也自不量力地借鉴ES的 EsThreadPoolExecutor ...
- 【Exception—WebForm】当应用程序不是以 UserInteractive 模式运行时显示模式对话框或窗体是无效操作。请指定 ServiceNotification 或 DefaultDesktopOnly 样式,以显示服务应用程序发出的通知。
最近做的项目现在发布到服务器上开始测试了,本地好好的程序,到服务器上却报异常了: 当应用程序不是以 UserInteractive 模式运行时显示模式对话框或窗体是无效操作.请指定 ServiceNo ...
- c/c++编译时,指定程序运行时查找的动态链接库路径
http://blog.csdn.net/tsxw24/article/details/10220735 c/c++编译时,指定程序运行时查找的动态链接库路径 分类: c/c++ linux 2013 ...
随机推荐
- 【忍者算法】从风扇叶片到数组轮转:探索轮转数组问题|LeetCode 189 轮转数组
从风扇叶片到数组轮转:探索轮转数组问题 生活中的算法 想象你在看一个风扇缓缓转动,每次转动三个叶片的距离.原本在上方的叶片转到了右侧,原本在右侧的叶片转到了下方...这就是一个生动的轮转过程.再比如, ...
- .NET中优雅使用Patch: JsonPatch
引言 在现代 Web API 开发中,我们经常需要对资源进行部分更新(Partial Update).传统的 PUT 请求会要求发送整个对象,而 PATCH 请求可以仅发送需要更新的字段.ASP.NE ...
- Sa-Token v1.40.0 发布 🚀,来看看有没有令你心动的功能!
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证.权限认证.单点登录.OAuth2.0.微服务网关鉴权 等一系列权限相关问题. 目前最新版本 v1.40.0 已发布至 Mav ...
- .NET周刊【2月第1期 2025-02-02】
国内文章 dotnet 9 已知问题 默认开启 CET 导致进程崩溃 https://www.cnblogs.com/lindexi/p/18700406 本文记录 dotnet 9 的一个已知且当前 ...
- Flink-cdc同步mysql到iceberg丢失数据排查
一.获取任务信息 任务id:i01f51582-d8be-4262-aefa-000000 任务名称:ods_test1234 丢失的数据时间:2024-09-16 09:28:47 二.数据同步查看 ...
- Atcoder ABC390F Double Sum 3 题解 [ 绿 ] [ 贡献思维 ] [ 计数 ]
Double Sum 3:简单计数题. 思路 首先考虑单个区间的 \(f\) 值如何计算,显然等于值域上连续段的个数.那么我们进一步观察值域上连续段的性质,发现一个连续段的开头一定满足比开头小 \(1 ...
- 安川MOTOMAN机器人NX100维修的注意事项
安川MOTOMAN机器人NX100维修,操作人员安全注意事项 整个机器人的最大动作范围内均具有潜在的危险性. 为机器人工作的所有人员 (安全管理员.安装人员.操作人员和机器人维修人员) 必须时刻树 ...
- Java - 高射炮打蚊子(第二弹)
题记部分 01 || 面试题 001 || 什么是JVM JVM(Java虚拟机)是Java程序运行的环境,它是一个抽象的计算机,包括指令集.寄存器集.堆栈.垃圾回收等.JVM屏蔽了与具体操作系统平台 ...
- Python基础笔记-while、字符串格式化、运算符、基础概念与数据类型
前言 !!!注意:本系列所写的文章全部是学习笔记,来自于观看视频的笔记记录,防止丢失.观看的视频笔记来自于:哔哩哔哩武沛齐老师的视频:2022 Python的web开发(完整版) 入门全套教程,零基础 ...
- STM32实战——DHT11温湿度获取并展示
介绍 DHT11数字温湿度传感器是一款含有已校准数字信号输出的温湿度复合传感器,包括一个电阻式感湿元件和一个NTC测温元件,可以用来测量温度和湿度. 硬件连线 注意 本实验使用STM32F103C8T ...