news 2026/9/20 21:02:55

.NET 运行时 User Secrets 配置提供程序(Microsoft.Extensions.Configuration.UserSecrets)原理与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
.NET 运行时 User Secrets 配置提供程序(Microsoft.Extensions.Configuration.UserSecrets)原理与实战

.NET 运行时 User Secrets 配置提供程序(Microsoft.Extensions.Configuration.UserSecrets)原理与实战

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

导读

本文以 .NET 运行时仓库 中 Microsoft.Extensions.Configuration.UserSecrets 组件 的源码与测试为基础,系统讲解 User Secrets(用户机密)配置提供程序的设计原理与使用方式:它如何在开发阶段用本地磁盘上的secrets.json覆盖应用配置、UserSecretsId是如何由 MSBuild 自动写入程序集特性、机密文件在 Windows/macOS/Linux 上分别存放在哪里,以及如何通过AddUserSecrets扩展方法把机密接入IConfigurationBuilder。读完本文,你将掌握 User Secrets 的完整使用姿势与底层实现细节,能够正确排查"机密未加载"类问题。

什么是 User Secrets 配置提供程序

Microsoft.Extensions.Configuration.UserSecrets是 .NET 配置体系(Microsoft.Extensions.Configuration)中一个面向开发环境的配置提供程序实现。它的核心思想是:把不应提交进源码仓库的敏感配置(连接字符串、API Key、账号密码等)以 JSON 形式存储在用户主目录下、版本控制之外的本地文件中,然后在构建配置时作为"覆盖层"叠加到现有配置链上。

正如该组件的 README 所述,它实现的是 ASP.NET Core 应用机密(app secrets)机制;而组件自身定位于:贡献门槛 中明确"新特性、新 API、缺陷修复与性能改动均可接受",API 与功能已成熟,但仍会不时扩展。

该组件在仓库中的源码组织如下:

  • 核心扩展方法:UserSecretsConfigurationExtensions.cs
  • 机密文件路径解析:PathHelper.cs
  • 程序集级标识特性:UserSecretsIdAttribute.cs
  • 构建期注入逻辑:buildTransitive/Microsoft.Extensions.Configuration.UserSecrets.targets
  • 测试套件:tests/

如何引用与部署

从该组件的 csproj 可以看到,它的目标框架覆盖了NetCoreAppCurrentNetCoreAppPreviousNetCoreAppMinimumnetstandard2.0以及$(NetFrameworkMinimum),因此既可以在现代 .NET 应用中使用,也可以被旧框架项目引用。

它的部署形态有两种(见 README 的 Deployment 小节):

  1. 随 ASP.NET Core 共享框架分发:在 ASP.NET Core 应用中无需显式添加 NuGet 引用即可使用;
  2. 以 OOB(out-of-band)NuGet 包独立发布Microsoft.Extensions.Configuration.UserSecrets包可被任意项目直接引用,例如控制台应用。

依赖关系上,该包以项目引用形式依赖 Microsoft.Extensions.Configuration.Json 与 Microsoft.Extensions.FileProviders.Physical;对非当前目标框架还额外引用Microsoft.Extensions.Configuration.AbstractionsMicrosoft.Extensions.FileProviders.Abstractions(见 csproj)。这从侧面印证:User Secrets 的底层就是"用物理文件提供程序读取 JSON 配置文件",与AddJsonFile共享同一套加载管道。

核心 API:AddUserSecrets 扩展方法

组件对外暴露的全部 API 都集中在UserSecretsConfigurationExtensions静态类中(ref 程序集 是权威的公开 API 清单),共有 7 个AddUserSecrets重载,全部以IConfigurationBuilderthis参数:

重载说明
AddUserSecrets<T>()从类型T所在程序集读取UserSecretsIdAttribute,机密缺失时静默跳过(optional: true
AddUserSecrets<T>(bool optional)同上,可控制是否允许缺失
AddUserSecrets<T>(bool optional, bool reloadOnChange)同上,额外支持文件变更时自动重载
AddUserSecrets(Assembly assembly)从指定程序集读取特性,默认 optional
AddUserSecrets(Assembly assembly, bool optional)从指定程序集读取特性,可控 optional
AddUserSecrets(Assembly assembly, bool optional, bool reloadOnChange)完整版:程序集 + optional + reloadOnChange
AddUserSecrets(string userSecretsId)直接指定机密 ID,optional恒为 true
AddUserSecrets(string userSecretsId, bool reloadOnChange)直接指定机密 ID + 是否热重载

参数语义

  • optional:决定当机密"缺失"时的行为。缺失分两种情况——程序集上没有UserSecretsIdAttribute(仅对按程序集查找的重载有效),或机密文件不存在。当optionalfalse时,前者抛出InvalidOperationException,后者在Build()时因 JSON 文件缺失而抛出FileNotFoundException;为true时静默返回,不产生任何配置项。默认值均为true
  • reloadOnChange:是否在secrets.json文件变化后自动重新加载配置,默认false
  • userSecretsId:唯一标识一个机密集合的字符串。底层用它定位存储目录与文件名(详见后文路径解析),因此必须是不含非法文件名字符的合法目录名

按程序集查找的调用链

AddUserSecrets(Assembly assembly, bool optional, bool reloadOnChange)为例(源码):

  1. 空值校验configurationassemblyArgumentNullException.ThrowIfNull);
  2. 通过assembly.GetCustomAttribute<UserSecretsIdAttribute>()反射读取程序集特性;
  3. 若特性存在,调用内部方法AddUserSecretsInternal(configuration, attribute.UserSecretsId, optional, reloadOnChange)继续;
  4. 若特性不存在且optionalfalse,抛出InvalidOperationException,错误信息来自 Strings.resx 中的Error_Missing_UserSecretsIdAttribute,会明确提示"检查项目是否设置了UserSecretsId构建属性,若已设置请确认引用了本包";
  5. 若特性不存在且optionaltrue,直接返回原 builder,不加载任何配置。

泛型重载AddUserSecrets<T>()本质上是AddUserSecrets(typeof(T).Assembly, optional: true, reloadOnChange: false)的语法糖(源码)。

底层加载:复用 AddJsonFile

所有重载最终汇入AddSecretsFile(源码):

private static IConfigurationBuilder AddSecretsFile(IConfigurationBuilder configuration, string secretPath, bool optional, bool reloadOnChange) { if (string.IsNullOrEmpty(secretPath)) { return configuration; } string? directoryPath = Path.GetDirectoryName(secretPath); PhysicalFileProvider? fileProvider = Directory.Exists(directoryPath) ? new PhysicalFileProvider(directoryPath) : null; return configuration.AddJsonFile(fileProvider, PathHelper.SecretsFileName, optional, reloadOnChange); }

可见它调用的正是Microsoft.Extensions.Configuration.Json包里的AddJsonFile(fileProvider, fileName, optional, reloadOnChange)重载——因此机密文件也遵循 JSON 配置的层级键(如"Facebook:PLACEHOLDER")与AsEnumerable()展开规则。当机密目录不存在时,fileProvidernullAddJsonFile在 optional 模式下会安全跳过。

UserSecretsId 的构建期注入

要让AddUserSecrets<T>()正常工作,程序集上必须存在UserSecretsIdAttribute。该特性定义在 UserSecretsIdAttribute.cs:

  • 只能标注在程序集上(AttributeTargets.Assembly),不可继承、不可重复(Inherited = false, AllowMultiple = false);
  • 构造时校验 ID 非空,暴露只读属性UserSecretsId
  • 其 XML 注释明确指出:在绝大多数情况下,该特性由 NuGet 包内置的 MSBuild targets 在编译期自动生成,值来自 MSBuild 属性UserSecretsId

targets 的注入逻辑

Microsoft.Extensions.Configuration.UserSecrets.targets 完整内容如下:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <MSBuildAllProjects Condition="'$(MSBuildVersion)' == '' Or '$(MSBuildVersion)' &lt; '16.0'">$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects> <GenerateUserSecretsAttribute Condition="'$(GenerateUserSecretsAttribute)'==''">true</GenerateUserSecretsAttribute> </PropertyGroup> <ItemGroup Condition=" '$(UserSecretsId)' != '' AND '$(GenerateUserSecretsAttribute)' != 'false' "> <AssemblyAttribute Include="Microsoft.Extensions.Configuration.UserSecrets.UserSecretsIdAttribute"> <_Parameter1>$(UserSecretsId.Trim())</_Parameter1> </AssemblyAttribute> </ItemGroup> </Project>

关键点:

  • 触发条件:项目定义了UserSecretsId属性(非空),且GenerateUserSecretsAttribute未显式设为false
  • 默认开启GenerateUserSecretsAttribute默认为true
  • 生成方式:向编译项AssemblyAttribute追加一条特性,构造参数_Parameter1$(UserSecretsId.Trim())——注意它会先对 ID 做 Trim 去空白,因此即便在 csproj 里写成多行缩进的UserSecretsId也能得到干净的 ID(MsBuildTargetTest的测试工程正是用了带换行缩进的值"xyz123");
  • 构建增量友好:该文件是 MSBuild 的AssemblyAttribute项,由 SDK 负责生成obj/.../AssemblyInfo.cs,重复构建不会重复生成(测试中专门断言了第二次构建后文件LastWriteTimeUtc不变,见下文)。

props:向 IDE 暴露能力

同目录下的 Microsoft.Extensions.Configuration.UserSecrets.props 只做一件事:向项目注入ProjectCapability

<ItemGroup> <!-- This capability represents the UserSecretsID + secrets.json approach to storing local user secrets. --> <ProjectCapability Include="LocalUserSecrets" /> </ItemGroup>

这让 Visual Studio 等 IDE 能够识别"该项目启用了本地用户机密"这一能力,从而在 UI 上提供"管理用户机密"等入口。两个文件通过 csproj 的 Content 项 打包到buildTransitive\netstandard2.0\(以及NetFrameworkMinimumNetCoreAppMinimum对应目录),因此引用该包的项目会自动导入 targets 与 props,实现"零配置"注入。

机密文件的存储路径解析:PathHelper

PathHelper.cs 负责把userSecretsId映射为磁盘上的secrets.json完整路径,其算法是理解整个机制的关键:

平台路径规则

环境机密文件路径
Windows(存在APPDATA%APPDATA%\Microsoft\UserSecrets\<userSecretsId>\secrets.json
macOS / Linux(存在HOME~/.microsoft/usersecrets/<userSecretsId>/secrets.json
APPDATA/HOME依次回退Environment.SpecialFolder.ApplicationDataUserProfile,最后是DOTNET_USER_SECRETS_FALLBACK_DIR环境变量

其中文件名常量定义在源码中:internal const string SecretsFileName = "secrets.json";

源码级解析顺序

InternalGetSecretsPathFromSecretsId(源码)的执行步骤:

  1. 校验 ID:为空抛ArgumentException;若包含Path.GetInvalidFileNameChars()中的任意字符,抛InvalidOperationException,错误信息Error_Invalid_Character_In_UserSecrets_Id会精确指出非法字符及其索引("Invalid character '{0}' found in the user secrets ID at index '{1}'.")。这也是为什么UserSecretsId建议使用 GUID 或项目名-随机串这类纯安全字符的原因。
  2. 确定根目录:优先读环境变量APPDATA(Windows)→HOME(macOS/Linux)。对于 iOS/tvOS/MacCatalyst,HOME指向应用沙盒容器根目录且不可写,因此在NET下会强制把home置为null,转而走SpecialFolder回退链。最后兜底是DOTNET_USER_SECRETS_FALLBACK_DIR——这是官方注释明说的"逃生舱"(escape hatch),用于自定义存储根目录。
  3. 根目录仍为空:若throwIfNoRoottrueAddUserSecrets(assembly, optional: false)场景),抛InvalidOperationExceptionError_Missing_UserSecretsLocation,提示设置DOTNET_USER_SECRETS_FALLBACK_DIR);否则返回空字符串,上层AddSecretsFile检测到空路径后直接返回、不加载。
  4. 拼接路径:Windows 风格(appData非空)拼Microsoft\UserSecrets\...,Unix 风格拼.microsoft/usersecrets/...。注意大小写差异Microsoft(Windows)vs.microsoft(macOS/Linux),这是由该目录历史上先在 Windows 上定义、后在 Unix 上采用点前缀约定的结果。

PathHelper.GetSecretsPathFromSecretsId是公开 API,恒以throwIfNoRoot: true调用,因此即使只是查询路径,在完全无法确定存储位置时也会抛异常。

测试验证:行为即规范

组件测试位于 tests/,覆盖三个维度:

1. 配置扩展行为(ConfigurationExtensionTest.cs)

ConfigurationExtensionTest.cs 通过程序集级[assembly: UserSecretsId("d6076a6d3ab24c00b2511f10a56c68cc")]模拟真实项目,验证:

  • AddUserSecrets(typeof(...).Assembly)AddUserSecrets<T>()都能从程序集特性发现 ID 并读到写入的机密值(AddUserSecrets_FindsAssemblyAttributeAddUserSecrets_FindsAssemblyAttributeFromType);
  • 程序集无特性时:optional: falseInvalidOperationException且错误信息精确匹配资源字符串(AddUserSecrets_ThrowsIfAssemblyAttributeFromType);默认(optional)不抛异常、配置为空(AddUserSecrets_DoesNotThrowsIfOptionalByDefault);
  • 特性存在但secrets.json不存在时,optional: falseBuild()时抛FileNotFoundExceptionAddUserSecrets_DoesThrowsIfNotOptionalAndSecretDoesNotExist);
  • 直接传userSecretsId时,可读取嵌套键(如"Facebook:PLACEHOLDER"),且文件不存在时静默返回nullAddUserSecrets_With_SecretsId_Passed_ExplicitlyAddUserSecrets_Does_Not_Fail_On_Non_Existing_File)。

2. MSBuild 注入(MsBuildTargetTest.cs)

MsBuildTargetTest.cs 在临时目录构造一个"极简dotnet new风格"的 csproj(设置了带换行的<UserSecretsId>xyz123</UserSecretsId>),模拟 NuGet 导入 targets 后真实执行dotnet restoredotnet build,断言:

  • 生成的obj/Debug/<tfm>/test.AssemblyInfo.cs中包含assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsIdAttribute("xyz123")
  • 二次构建不重新生成该文件(增量构建友好,LastWriteTimeUtc保持不变)。

这从端到端角度验证了 targets 中$(UserSecretsId.Trim())的去空白行为与 AssemblyAttribute 注入全链路。测试同时准备了.csproj/.fsproj两套工程(F# 用例因上游问题暂被Skip),说明该机制对 C#/F# SDK 项目均适用。

3. 路径解析(PathHelperTest.cs)

PathHelperTest.cs 验证:

  • APPDATA/HOME下得到的路径与"根目录 +Microsoft/UserSecrets/.microsoft/usersecrets/+ ID +secrets.json"的期望完全一致(Gives_Correct_Secret_Path);
  • 当 ID 含非法字符(Path.GetInvalidPathChars()Path.GetInvalidFileNameChars()全集)时一律抛InvalidOperationExceptionThrows_If_UserSecretId_Contains_Invalid_Characters)。

使用示例:从配置到读取

把以上机制串起来,一个标准的开发期机密工作流如下。

1. 在 csproj 中声明机密 ID(一般由 IDE 的"管理用户机密"功能或dotnet user-secrets init写入):

<PropertyGroup> <UserSecretsId>my-app-3f2b1c0d-9a8e-4f6b-8c1d-2e3f4a5b6c7d</UserSecretsId> </PropertyGroup>

2. 写入机密值(例如通过dotnet user-secrets set,或直接编辑机密文件):

{ "ConnectionStrings:Default": "Server=localhost;Database=DevDb;User Id=sa;Password=dev-only-pwd", "ExternalApi:Key": "sk-dev-xxxx" }

3. 在程序入口把 User Secrets 接入配置链

using Microsoft.Extensions.Configuration; var builder = new ConfigurationBuilder() .AddUserSecrets<Program>(); // 从 Program 所在程序集的 UserSecretsIdAttribute 读取 ID // 或:.AddUserSecrets("my-app-3f2b1c0d-9a8e-4f6b-8c1d-2e3f4a5b6c7d"); var configuration = builder.Build(); var connStr = configuration["ConnectionStrings:Default"];

加载顺序上,AddUserSecrets通常放在环境相关或敏感度较高的提供程序位置,使机密值覆盖同键的appsettings.json,同时仍可被命令行参数、环境变量等更靠后的提供程序覆盖,形成"默认值 → 开发机密 → 环境/运行时覆盖"的优先级链。

常见问题与最佳实践

  • "Could not find 'UserSecretsIdAttribute' on assembly":程序集上没有特性且使用了optional: false。按 Strings.resx 的提示排查:确认项目已设置UserSecretsId属性,并确认项目确实引用了本包(targets 才会被导入)。
  • 机密加载为空:先检查PathHelper.GetSecretsPathFromSecretsId(id)返回的路径与编辑器打开的secrets.json是否一致——两者由同一算法定位,%APPDATA%/HOME不一致(如服务账户与交互用户不同)时最易出现"看不到机密"。
  • UserSecretsId 命名:仅允许文件名字符,推荐用 GUID 或"项目名-随机串",避免包含/\:*?"<>|等字符(PathHelperTest 对非法字符全集做了断言)。
  • 环境变量被赋值为空字符串:路径解析中APPDATA/HOME若存在但为空,会被??跳过(空串不触发回退),需注意 CI/容器环境变量残留。
  • 生产环境不要用 User Secrets:它存储于本机用户目录、明文 JSON,设计目标仅限开发阶段覆盖配置;生产机密应使用环境变量、Azure Key Vault、Secret Manager 等方案。reloadOnChange仅对开发调试有帮助,生产环境不建议依赖它承载机密热更新。

小结

Microsoft.Extensions.Configuration.UserSecrets用极简的设计解决了开发期敏感配置的落地问题:UserSecretsIdAttribute在编译期由 MSBuild targets 自动注入,PathHelper依据APPDATA/HOME/DOTNET_USER_SECRETS_FALLBACK_DIR等线索定位本机密文件,AddUserSecrets系列扩展方法复用AddJsonFile管道完成加载——三个环节各司其职,配合 测试套件 对异常分支(无特性、文件缺失、非法字符、增量构建)的严格约束,构成了一个成熟、可靠且可预测的开发期配置覆盖方案。理解这条链路后,无论是日常使用还是排障,你都能迅速定位问题所在。

【免费下载链接】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/20 20:58:40

6款主流数据同步工具选型指南:从Canal到信创场景实操

数据库同步这件事&#xff0c;说起来简单&#xff0c;做起来坑多。我最早接触数据同步是在一个报表系统项目里&#xff0c;当时需要把业务库的数据实时搬到分析库&#xff0c;想着写个定时脚本轮询就完事了&#xff0c;结果上线第二天就出了数据不一致的问题——业务库更新了一…

作者头像 李华
网站建设 2026/9/20 20:58:38

LDM核心组件深度拆解:从潜空间压缩到采样调度策略

1. 从像素级暴力到潜空间优雅&#xff1a;LDM到底革了什么命搞AI绘图有一段时间的朋友&#xff0c;多少都会遇到一个尴尬场景&#xff1a;Local Diffuser跑一张512x512的图&#xff0c;显存占用轻松吃掉8GB以上&#xff0c;出图一张要等十几秒甚至更久。两年前的我大概不会想到…

作者头像 李华