.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 可以看到,它的目标框架覆盖了NetCoreAppCurrent、NetCoreAppPrevious、NetCoreAppMinimum、netstandard2.0以及$(NetFrameworkMinimum),因此既可以在现代 .NET 应用中使用,也可以被旧框架项目引用。
它的部署形态有两种(见 README 的 Deployment 小节):
- 随 ASP.NET Core 共享框架分发:在 ASP.NET Core 应用中无需显式添加 NuGet 引用即可使用;
- 以 OOB(out-of-band)NuGet 包独立发布:
Microsoft.Extensions.Configuration.UserSecrets包可被任意项目直接引用,例如控制台应用。
依赖关系上,该包以项目引用形式依赖 Microsoft.Extensions.Configuration.Json 与 Microsoft.Extensions.FileProviders.Physical;对非当前目标框架还额外引用Microsoft.Extensions.Configuration.Abstractions与Microsoft.Extensions.FileProviders.Abstractions(见 csproj)。这从侧面印证:User Secrets 的底层就是"用物理文件提供程序读取 JSON 配置文件",与AddJsonFile共享同一套加载管道。
核心 API:AddUserSecrets 扩展方法
组件对外暴露的全部 API 都集中在UserSecretsConfigurationExtensions静态类中(ref 程序集 是权威的公开 API 清单),共有 7 个AddUserSecrets重载,全部以IConfigurationBuilder为this参数:
| 重载 | 说明 |
|---|---|
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(仅对按程序集查找的重载有效),或机密文件不存在。当optional为false时,前者抛出InvalidOperationException,后者在Build()时因 JSON 文件缺失而抛出FileNotFoundException;为true时静默返回,不产生任何配置项。默认值均为true。reloadOnChange:是否在secrets.json文件变化后自动重新加载配置,默认false。userSecretsId:唯一标识一个机密集合的字符串。底层用它定位存储目录与文件名(详见后文路径解析),因此必须是不含非法文件名字符的合法目录名。
按程序集查找的调用链
以AddUserSecrets(Assembly assembly, bool optional, bool reloadOnChange)为例(源码):
- 空值校验
configuration与assembly(ArgumentNullException.ThrowIfNull); - 通过
assembly.GetCustomAttribute<UserSecretsIdAttribute>()反射读取程序集特性; - 若特性存在,调用内部方法
AddUserSecretsInternal(configuration, attribute.UserSecretsId, optional, reloadOnChange)继续; - 若特性不存在且
optional为false,抛出InvalidOperationException,错误信息来自 Strings.resx 中的Error_Missing_UserSecretsIdAttribute,会明确提示"检查项目是否设置了UserSecretsId构建属性,若已设置请确认引用了本包"; - 若特性不存在且
optional为true,直接返回原 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()展开规则。当机密目录不存在时,fileProvider为null,AddJsonFile在 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)' < '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\(以及NetFrameworkMinimum、NetCoreAppMinimum对应目录),因此引用该包的项目会自动导入 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.ApplicationData、UserProfile,最后是DOTNET_USER_SECRETS_FALLBACK_DIR环境变量 |
其中文件名常量定义在源码中:internal const string SecretsFileName = "secrets.json";。
源码级解析顺序
InternalGetSecretsPathFromSecretsId(源码)的执行步骤:
- 校验 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 或项目名-随机串这类纯安全字符的原因。 - 确定根目录:优先读环境变量
APPDATA(Windows)→HOME(macOS/Linux)。对于 iOS/tvOS/MacCatalyst,HOME指向应用沙盒容器根目录且不可写,因此在NET下会强制把home置为null,转而走SpecialFolder回退链。最后兜底是DOTNET_USER_SECRETS_FALLBACK_DIR——这是官方注释明说的"逃生舱"(escape hatch),用于自定义存储根目录。 - 根目录仍为空:若
throwIfNoRoot为true(AddUserSecrets(assembly, optional: false)场景),抛InvalidOperationException(Error_Missing_UserSecretsLocation,提示设置DOTNET_USER_SECRETS_FALLBACK_DIR);否则返回空字符串,上层AddSecretsFile检测到空路径后直接返回、不加载。 - 拼接路径: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_FindsAssemblyAttribute、AddUserSecrets_FindsAssemblyAttributeFromType);- 程序集无特性时:
optional: false抛InvalidOperationException且错误信息精确匹配资源字符串(AddUserSecrets_ThrowsIfAssemblyAttributeFromType);默认(optional)不抛异常、配置为空(AddUserSecrets_DoesNotThrowsIfOptionalByDefault); - 特性存在但
secrets.json不存在时,optional: false在Build()时抛FileNotFoundException(AddUserSecrets_DoesThrowsIfNotOptionalAndSecretDoesNotExist); - 直接传
userSecretsId时,可读取嵌套键(如"Facebook:PLACEHOLDER"),且文件不存在时静默返回null(AddUserSecrets_With_SecretsId_Passed_Explicitly、AddUserSecrets_Does_Not_Fail_On_Non_Existing_File)。
2. MSBuild 注入(MsBuildTargetTest.cs)
MsBuildTargetTest.cs 在临时目录构造一个"极简dotnet new风格"的 csproj(设置了带换行的<UserSecretsId>xyz123</UserSecretsId>),模拟 NuGet 导入 targets 后真实执行dotnet restore与dotnet 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()全集)时一律抛InvalidOperationException(Throws_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),仅供参考