不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
· 小村 豪 · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, 配置管理, Windows 开发, 技术咨询
「想按环境切换连接字符串」「想保存每个用户的显示设置」「把 API 密钥写进了 appsettings.json」。Windows 业务应用的配置管理,大家多半都能做到放一个 appsettings.json、用 IConfiguration 读取,但再往下──环境差异化设置、可写入配置的存放位置、敏感信息的处理、运行期配置变更──却常常在没有理清楚的情况下就这样投入运行。
本文从 .NET 配置系统的基础 IConfiguration 与提供程序的叠加开始,依次整理环境差异化设置、通过 IOptions 模式以类型安全的方式接收配置、可写入配置的存放位置、敏感信息的处理、运行期配置变更的现实限制,以及从 app.config/Settings.settings 迁移的对应表,按实务上容易犹豫的顺序来说明。
1. 先下结论
配置管理的判断,要从按「配置的种类」决定「存放位置」开始。先用一张判断表整理全貌。
| 配置的种类 | 具体示例 | 存放位置首选 | 理由 |
|---|---|---|---|
| 应用默认值 | 日志级别的默认值、UI 的默认参数 | appsettings.json |
随构建产物发布、不依赖环境的公共值 |
| 环境差异化设置 | 验证环境/生产环境会不同的连接字符串、API 端点 | appsettings.{Environment}.json + 环境变量 |
可直接沿用默认的覆盖顺序 |
| 用户级设置 | 上次打开的文件夹、窗口位置、个人化显示设置 | %LOCALAPPDATA%(或 %APPDATA%)下面的自定义文件 |
需要以用户为单位可写入的区域 |
| 机器级设置(全体用户共享) | 设备的 COM 端口号、许可证服务器地址 | %ProgramData% 下面的自定义文件 |
由管理员针对每台机器设置一次,所有用户共享 |
| 敏感信息 | 连接字符串中的密码、API 密钥、token | DPAPI 保护的文件(生产环境)、user-secrets(仅限开发时) | 不以明文存放。不要把开发用机制挪用到生产环境 |
| 运行期会变动的值 | 功能开关、日志级别的动态变更 | appsettings.json(reloadOnChange) + IOptionsMonitor |
仅限于希望不重启也能生效的值 |
以这张表为基础,先写出结论。
- 配置的优先级遵循「后加入的提供程序优先」这一条规则。 默认会按
appsettings.json→appsettings.{Environment}.json→ (仅开发环境)user secrets → 环境变量 → 命令行参数的顺序读取,如果有相同的键,会以后读到的值覆盖。只要记住这个顺序,「明明写进了 json 却没有生效」的问题大多都能解释清楚。1 - 只要使用
Host.CreateApplicationBuilder,这个层次结构就会预先帮你准备好,不必自行搭建。 Windows Forms/WPF 这类桌面应用,只要使用 Generic Host,也能享受同样的默认值优势。23 - 环境切换通过
DOTNET_ENVIRONMENT(或ASPNETCORE_ENVIRONMENT)进行。 在WebApplication系列中,DOTNET_ENVIRONMENT优先,两者都未设置时默认值为Production。桌面应用或 Windows 服务,必须先设计好「由谁、如何把这个环境变量传给进程」。4 - 配置不要用 DTO 接收,而是用
IOptions<T>家族以类型接收。 只需要在启动时获取一次就用IOptions<T>,希望不重启也能生效变更就用IOptionsMonitor<T>。如果不理解两者的差异就随意混用,「不该变的值变了」「该变的值却没变」这两种事故都可能发生。5 - 配置错误要在启动时就让程序失败,而不是在运行时才出错。 搭配
ValidateDataAnnotations()与ValidateOnStart(),配置错误就会变成「启动后立即以异常方式被发现」,避免「生产环境半夜才因为 NullReferenceException 而察觉」的事故。6 - 敏感信息不要以明文放进
appsettings.json。 开发时使用 user-secrets,生产环境则使用 DPAPI(ProtectedData)或凭据管理器。官方文档明确指出 user-secrets 没有加密,是开发专用的机制。78
2. .NET 配置系统的基础 ── IConfiguration 与提供程序
.NET 的配置系统,是在 IConfiguration 这个单一键值存储的外观之下,叠加多个配置提供程序来读取数据的机制。这种机制的优点是,可以把 JSON、环境变量、命令行参数、INI、XML、内存中的集合等性质不同的来源,统一整合到同一个 IConfiguration 接口之后。9
提供程序的叠加方式很简单,就是「后加入的优先」。如果同一个键存在于多个提供程序中,会以最后加入的提供程序的值为准。1
使用 Host.CreateApplicationBuilder(args) 时,会按下列顺序默认组成提供程序(数字越大优先级越高,也就是后者优先)。2
appsettings.jsonappsettings.{Environment}.json- Secret Manager(仅开发环境时)
- 环境变量
- 命令行参数
无论是控制台应用、Worker Service,还是 Windows Forms 应用,这个默认顺序都是通用的。以下是最小配置的示例。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// 默认已按上述优先级读取 appsettings.json / appsettings.{Environment}.json /
// 环境变量 / 命令行参数
string? connectionString = builder.Configuration.GetConnectionString("Main");
using IHost host = builder.Build();
await host.RunAsync();
即使是 Windows Forms 这类桌面应用,只要添加 Microsoft.Extensions.Hosting 包,也能直接沿用同一个 HostApplicationBuilder。正如本博客「Generic Host 是什么」一文所写,Generic Host 是能一并处理配置・DI・日志的基础,没有理由因为是桌面应用就放弃配置系统带来的好处。伴随常驻处理的应用,具体的搭建方式请参阅「在桌面应用中使用 Generic Host + BackgroundService」。
环境变量的处理有个要注意的地方。主机设置(内容根目录、环境名称等)会从带有 DOTNET_ 前缀的环境变量读取,但这不会用在应用配置(通过 IConfiguration 读取的一般值)上。 想让应用配置读取的环境变量,会通过不带前缀的 AddEnvironmentVariables() 并入默认顺序。10 如果想加上自定义前缀,可以像下面这样明确添加。
using Microsoft.Extensions.Configuration;
// 加在默认提供程序之后,因此优先级最高
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");
3. 环境差异化设置 ── appsettings.{Environment}.json
想按环境改变连接字符串或端点时,可以使用 appsettings.Development.json / appsettings.Staging.json / appsettings.Production.json 这类环境文件。重要的是,这个文件会被当作「覆盖」基础 appsettings.json 的差异文件来处理,不必重写所有项目,只需要写出随环境而异的值即可。1
环境名称由 DOTNET_ENVIRONMENT 或 ASPNETCORE_ENVIRONMENT 这个环境变量决定。使用 WebApplication 时,DOTNET_ENVIRONMENT 的值优先于 ASPNETCORE_ENVIRONMENT,两者都未设置时默认值为 Production。在 Windows 上环境变量名称不区分大小写,但在 Linux 上会区分,如果考虑容器化,统一写法会更安全。4
这里会出现的问题是,在桌面应用或 Windows 服务中,「如何传递环境变量」本身就是一项工作。 ASP.NET Core 的 launchSettings.json 这类开发时机制只在本地开发时使用,因此生产环境运行时,需要另外准备切换环境的手段。代表性的传递方式有以下 3 种。
- 设置为机器级的环境变量。 像
setx DOTNET_ENVIRONMENT Production /M这样永久设置,就会传递给该机器上运行的所有进程。但如果想在一台机器上同时运行多个环境的应用,这种方式并不合适。 - 在 Windows 服务的启动流程中传递环境变量。 以服务常驻运行时,是在跟交互式用户会话不同的上下文中启动的,因此不会传递用户环境变量。关于执行账户与会话分离的细节,请参阅「Windows 服务的创建方式与运维」。比较现实的做法是使用机器级的环境变量,或是把服务可执行文件做成一个薄的包装批处理,先
SET再启动本体。 - 通过任务计划程序启动时,用命令行参数传递比较可靠。 任务计划程序的「操作」没有直接设置环境变量的界面,因此改用像
--environment Production这样的命令行参数传递,再用AddCommandLine(args)读取,出错的概率较低。任务计划程序特有的执行账户・登录类型特性,整理在「任务计划程序的任务没有运行、以 0x1 结束」一文中。
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var options = new HostApplicationBuilderSettings
{
Args = args,
// 针对环境变量无法传递的执行路径(例如任务计划程序),
// 也允许通过命令行参数切换环境
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);
Console.WriteLine($"当前环境: {builder.Environment.EnvironmentName}");
4. 选项模式 ── 以类型接收配置
像 IConfiguration["Key:SubKey"] 这种通过字符串键读取的方式,有个弱点:打错字不容易察觉,而且嵌套层级一深就很难追踪。实务上的基本做法是使用选项模式(Options Pattern),把配置绑定到 POCO 上以类型接收。
public sealed class ExternalApiOptions
{
public const string SectionName = "ExternalApi";
public required string BaseUrl { get; set; }
public required string ApiKey { get; set; }
public int TimeoutSeconds { get; set; } = 30;
}
注册与绑定的写法如下。
using Microsoft.Extensions.DependencyInjection;
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));
接收方式有 3 种接口,各自性质不同。5
| 接口 | 注册的生命周期 | 配置变更的反映 | 主要用途 |
|---|---|---|---|
IOptions<T> |
单例(Singleton) | 不会反映(仅在启动时计算一次) | 假设不会变动的配置。最简单 |
IOptionsSnapshot<T> |
作用域(Scoped) | 每次作用域重新创建时重新计算 | Web 的请求作用域等,作用域明确的场景 |
IOptionsMonitor<T> |
单例(Singleton) | 附带变更通知(OnChange),随时可获取最新值 |
常驻服务等想实时检测变更的场景 |
在像桌面应用或 Windows 服务这种没有 HTTP 请求作用域的应用中,即使使用 IOptionsSnapshot<T>,只要不自行创建作用域,实质上的行为就会跟 IOptions<T> 相同。如果想检测变更,就直接使用 IOptionsMonitor<T>,用这个原则来选择会减少犹豫。
配置值(BaseUrl・ApiKey・超时)每次调用都从 IOptionsMonitor 获取,但 HttpClient 本身则从 IHttpClientFactory 获取并重复使用。如果每次调用都 new 一个 HttpClient 再 Dispose,就会每次都废弃、重建内部的套接字与连接池,在高频率轮询或批处理中容易导致临时端口(ephemeral port)耗尽,因此即使值会变动,连接的重复使用仍应交给 IHttpClientFactory 负责,这是标准做法。
using Microsoft.Extensions.Options;
public sealed class ExternalApiClient(
IHttpClientFactory httpClientFactory,
IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
public async Task<string> FetchAsync(CancellationToken cancellationToken)
{
// 每次调用获取最新值。配置文件若已更新,会反映在这里
ExternalApiOptions current = optionsMonitor.CurrentValue;
// HttpClient 本身通过工厂获取,重复使用内部连接池
HttpClient client = httpClientFactory.CreateClient(nameof(ExternalApiClient));
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(current.BaseUrl), "status"));
request.Headers.Add("X-Api-Key", current.ApiKey);
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(current.TimeoutSeconds));
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
using HttpResponseMessage response = await client.SendAsync(request, linkedCts.Token);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
}
调用端则像 builder.Services.AddHttpClient(nameof(ExternalApiClient)); 这样注册具名的客户端。BaseUrl 或 ApiKey 这类会因配置变更而改变的值,每次请求时附加到 HttpRequestMessage 上;HttpClient 实例本身的创建・释放成本,则交给工厂负责,这就是双方的分工。
配置错误如果要等到运行期才以异常方式出现,排查起来会很花时间。搭配 DataAnnotations 的验证与 ValidateOnStart(),就能做到配置损坏的应用根本无法启动的设计。6
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;
public sealed class ExternalApiOptions
{
public const string SectionName = "ExternalApi";
[Required, Url]
public required string BaseUrl { get; set; }
[Required, MinLength(16)]
public required string ApiKey { get; set; }
[Range(1, 300)]
public int TimeoutSeconds { get; set; } = 30;
}
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart(); // 在主机启动时(StartAsync/RunAsync 时)、主机服务启动之前执行验证
如果不加上 ValidateOnStart(),验证会延迟到实际首次访问该选项的时间点。验证本身不是在 Build() 之后立即执行,而是在主机启动时(StartAsync/RunAsync)才执行,因此在只调用了 Build() 却还没调用 RunAsync() 的测试代码中,这个时间点验证还没有被执行,这一点请务必留意。为了避免「明明启动了,但一打开使用该配置的界面就出错」这种间歇性事故,业务应用的配置原则上都应该加上 ValidateOnStart()。6
5. 可写入的配置该放在哪里
appsettings.json 是用来存放「只读默认值」的地方,不是给应用自己改写配置的位置。 许多业务应用安装在 Program Files 下面,标准用户没有写入权限,因此运行时不是抛出异常,就是因 Windows 的文件系统虚拟化而让不同用户看到的内容出现偏差。
需要写入的配置,按性质可以分成以下几类。以 Environment.GetFolderPath 获取的特殊文件夹为基准,是比较安全的做法。11
| 存放位置 | 获取方式 | 用途 |
|---|---|---|
%LOCALAPPDATA%\公司名称\应用名称 |
Environment.SpecialFolder.LocalApplicationData |
用户级设置・数据的默认位置 |
%APPDATA%\公司名称\应用名称(Roaming) |
Environment.SpecialFolder.ApplicationData |
仅限在漫游配置文件环境中,需要跟随用户的设置 |
%ProgramData%\公司名称\应用名称 |
Environment.SpecialFolder.CommonApplicationData |
全体用户共享的机器级设置。需要 ACL 设计 |
using System;
using System.IO;
using System.Text.Json;
public sealed class UserSettingsStore
{
private readonly string _filePath;
public UserSettingsStore()
{
string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string dir = Path.Combine(root, "KomuraSoft", "MyApp");
Directory.CreateDirectory(dir);
_filePath = Path.Combine(dir, "user-settings.json");
}
public UserSettings Load()
{
if (!File.Exists(_filePath))
{
return new UserSettings();
}
string json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<UserSettings>(json) ?? new UserSettings();
}
public void Save(UserSettings settings)
{
string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
// 假设同一进程内的多重写入由调用端自行串行化
File.WriteAllText(_filePath, json);
}
}
public sealed class UserSettings
{
public string? LastOpenedFolder { get; set; }
public int WindowWidth { get; set; } = 1024;
public int WindowHeight { get; set; } = 768;
}
如果把用户级设置和机器级设置混在同一个文件里,在多个用户共用同一台机器的环境中,就会发生「因为 A 的设置而让 B 困扰」的事故。关于用户级・机器级的区分,以及 Windows 配置文件结构本身的理解,请参阅「Windows 用户配置文件的结构」;至于存储位置选择的整体判断表,也可以参考「Windows 应用数据存储位置的选择方式」。
6. 敏感信息 ── 连接字符串・API 密钥
把连接字符串中的密码或 API 密钥直接写进 appsettings.json,即使已经加进 .gitignore,实务上仍然有风险。通过共享文件夹发布、支持对接时要求对方发送文件、僵尸化的旧配置文件留在备份中──都可能让明文外泄。
开发时使用 Secret Manager(dotnet user-secrets)。但这只是为了改善开发体验的机制,并没有加密。 这些值会以 JSON 明文存储在 %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json,官方文档也明确指出「不应把它当作可信任的存储区,仅供开发使用」。把生产环境的敏感信息放进 user-secrets 再发布,是一种错误的用法。7
dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "开发用的值"
生产环境运行时,使用 Windows 提供的 DPAPI(Data Protection API),将数据绑定到用户或机器的凭据上进行加密。System.Security.Cryptography.ProtectedData 的 Protect/Unprotect 就是这个包装器,使用 DataProtectionScope.CurrentUser 时,只有在同一个用户账户登录、且在同一台机器上,才能解密。DPAPI 是 Windows 专属功能,在其他平台上会抛出 PlatformNotSupportedException,设计时也要考虑到这一点。8
using System.Security.Cryptography;
using System.Text;
public static class SecretProtector
{
// 混入表示用途的熵值,可避免跟其他目的所保护的数据混淆
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("MyApp.ExternalApi.ApiKey");
public static string Protect(string plainText)
{
byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
byte[] protectedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
return Convert.ToBase64String(protectedBytes);
}
public static string Unprotect(string protectedBase64)
{
byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
byte[] plainBytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(plainBytes);
}
}
DPAPI 保护的值该存在哪里、CurrentUser 和 LocalMachine 该选哪个作用域、多个用户共用同一台机器的业务应用会遇到哪些陷阱,这类实现层级的细节写在「Windows 应用的敏感信息存储 - 用 DPAPI 避免明文设置」中,实现时请参阅该文。
该以明文保留、还是不该保留的界线很简单。判断准则是「这个值一旦泄露,是否需要重新发行密码或 API 密钥、进行未授权访问调查、向监管机构报告」。 连接字符串中的主机名或端口号,泄露时实际损害通常较小,但其中内嵌的密码或身份验证 token,则永远是保护对象。如果把连接字符串拆成「服务器信息」与「凭据」两部分来组合,只把后者列为 DPAPI 保护对象,就能在代码层级把「以明文保留也无妨的部分」和「该保护的部分」分开。
7. 运行期配置变更 ── reloadOnChange 的现实
AddJsonFile 的默认调用(Host.CreateApplicationBuilder 内部所做的调用)已设为 reloadOnChange: true,因此 appsettings.json / appsettings.{Environment}.json 会检测文件变更并自动重新读取。实现方式是 PhysicalFileProvider 内部使用 FileSystemWatcher 来监视变更。12
这个机制在实务上有几个限制。
- 只有通过
IOptionsMonitor<T>(以及IOptionsSnapshot<T>)读取的值才会反映变更。IOptions<T>会一直保留启动时的值,即使改写文件,也仍是旧值。「直接编辑配置文件却没有生效」这类问题,多半是搞混了这两者所致。 FileSystemWatcher在 Docker 容器或网络共享等文件系统上,有时无法确实发出变更通知。 在这类环境中,可以将DOTNET_USE_POLLING_FILE_WATCHER环境变量设为true,切换成每 4 秒一次的轮询监视(间隔无法更改)。13 关于FileSystemWatcher本身遗漏事件・缓冲区溢出・事件重复等常见特性,整理在「FileSystemWatcher 实务指南」中。- 配置文件的变更通知,有可能针对一次文件变更触发多次。 如果应用端要做「检测到变更就重新执行耗时处理」这类实现,需要考虑对短时间内连续触发做防抖(debounce),或比较文件内容的哈希值,只处理实质上的变化。
不必永远以「不重启也能反映配置变更」为目标。日志级别或功能开关这类轻量的值,可以用 IOptionsMonitor 立即生效;但像数据库连接字符串或线程池大小这类,一旦变更就可能跟正在运行的资源产生矛盾的值,明确规定「需要重启才会生效」反而更安全。 能明确告知运维人员「这项配置保存后立即生效」「这项配置需要重启」的设计,比是否使用 reloadOnChange 更重要。
8. 从 app.config / Settings.settings 迁移的对应表
把 .NET Framework 时代的应用迁移到 .NET 时,配置方式也必须重新搭建。以下整理对应关系。14
| .NET Framework | .NET | 备注 |
|---|---|---|
App.config / Web.config 的 <appSettings> |
appsettings.json + IConfiguration |
可用 JSON 的嵌套结构自然表达层级结构 |
ConfigurationManager.AppSettings["Key"] |
builder.Configuration["Key"] 或 IOptions<T> |
从字符串访问改为类型化访问 |
ConfigurationManager.ConnectionStrings |
builder.Configuration.GetConnectionString("Name") |
ConnectionStrings 部分的约定保持不变 |
Settings.settings(用户作用域) |
自定义的 %LOCALAPPDATA% JSON 文件 |
没有像 ApplicationSettingsBase 那样的自动生成机制,需要自行序列化/保存 |
Settings.settings(应用作用域) |
appsettings.json |
视为只读默认值处理 |
<connectionStrings> 的加密(aspnet_regiis 等) |
DPAPI(ProtectedData) |
加密机制本身会改变。迁移时需要重新搭建 |
迁移时常踩的坑有 2 个。
第一个是,只要添加 System.Configuration.ConfigurationManager NuGet 包,读取 App.config 的代码就能直接沿用。这作为迁移的第一步是有效的手段,但不应就此放着不管,而应该把迁移到 appsettings.json 纳入计划。像日志提供程序这类周边库,也大多已经以 appsettings.json 为前提迁移,只为了读取 App.config 而保留旧机制的理由会越来越薄弱。14
第二个是 Settings.settings 的用户作用域设置。在 .NET Framework 中,只要调用 Properties.Settings.Default.Save(),就有自动保存用户级设置的机制,但 .NET 并没有对应的标准自动生成机制。迁移时需要准备像第 5 章那样的自定义存储类。
迁移的另一个方面──依赖库的兼容性、是否有 COM 集成、发布方式的重新审视等,配置管理以外需要确认的项目全貌,整理在「.NET Framework→.NET 迁移前检查清单」中,建议在迁移项目初期先浏览一次。
结语
.NET 的配置管理,是由 IConfiguration 的提供程序叠加、IOptions 家族的类型安全接收、环境变量的环境切换这 3 种机制组合而成。到这里为止,多数应用都能用相同的方式使用,但 Windows 业务应用特有的难题,集中在再往下的部分──可写入配置的存放位置、敏感信息的保护、运行期配置变更该允许到什么程度,以及该如何收拾 app.config 世代的资产。
从「appsettings.json 里什么都写」的状态再往前一步,按配置种类分开存放位置与处理方式。如果本文的判断表能成为这个整理过程的起点,那就是我们的荣幸。现有应用的配置管理盘点,或是从 app.config 迁移的方针咨询,往往需要实际看过配置文件和部署环境才能找到最佳解,如果犹豫不决,欢迎与我们洽谈。
相关文章
- Generic Host 是什么
- 在桌面应用中使用 Generic Host + BackgroundService
- 任务计划程序的任务没有运行、以 0x1 结束
- Windows 服务的创建方式与运维
- Windows 应用数据存储位置的选择方式
- Windows 用户配置文件的结构
- Windows 应用的敏感信息存储 - 用 DPAPI 避免明文设置
- FileSystemWatcher 实务指南
- .NET Framework→.NET 迁移前检查清单
相关咨询领域
合同会社小村软件处理 Windows 业务应用的配置管理设计、环境差异化设置的运维设计,以及现有 app.config 资产迁移方针的技术咨询。
参考链接
-
Microsoft Learn, Configuration in .NET - Alternative hosting approach. 说明
Host.CreateApplicationBuilder默认组成的配置提供程序优先级(命令行参数 → 环境变量 → 开发环境的 user secrets → appsettings.{Environment}.json → appsettings.json)。 ↩ ↩2 ↩3 -
Microsoft Learn, .NET Generic Host - Host builder settings. 说明
Host.CreateApplicationBuilder读取的主机配置(带DOTNET_前缀的环境变量、命令行参数)与应用配置(appsettings.json、appsettings.{Environment}.json、Secret Manager、环境变量、命令行参数)的默认顺序。 ↩ ↩2 -
Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. 说明在 Windows Forms 应用中集成 Generic Host、使用 DI・配置・日志的步骤。 ↩
-
Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. 说明
DOTNET_ENVIRONMENT与ASPNETCORE_ENVIRONMENT的关系、使用WebApplication时DOTNET_ENVIRONMENT优先、未设置时默认值为Production,以及 Windows 上环境变量名称不区分大小写、但 Linux 上会区分。 ↩ ↩2 -
Microsoft Learn, Options pattern in .NET - Options interfaces. 说明
IOptions<TOptions>・IOptionsSnapshot<TOptions>・IOptionsMonitor<TOptions>在生命周期、配置变更反映时机、对应功能上的差异。 ↩ ↩2 -
Microsoft Learn, Options pattern in .NET - Options validation. 说明通过
ValidateDataAnnotations()进行 DataAnnotations 验证,以及通过ValidateOnStart()(或AddOptionsWithValidateOnStart)进行启动时验证的设置方式。 ↩ ↩2 ↩3 -
Microsoft Learn, Safe storage of app secrets in development in ASP.NET Core - Use the Secret Manager tool. 说明 Secret Manager 不会加密敏感信息,而是以明文存放在
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json,且仅供开发使用,不应视为可信任的存储区。 ↩ ↩2 -
Microsoft Learn, ProtectedData Class. 说明包装 DPAPI(Data Protection API)的
ProtectedData.Protect/Unprotect方法、DataProtectionScope.CurrentUser/LocalMachine作用域的差异,以及该功能仅限 Windows、在其他平台会抛出PlatformNotSupportedException。 ↩ ↩2 -
Microsoft Learn, Configuration providers in .NET. 说明将 JSON・环境变量・命令行・INI・XML 等性质不同的配置来源整合到
IConfiguration之后的配置提供程序机制。 ↩ -
Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. 说明默认配置会将带
DOTNET_前缀的环境变量・命令行参数读入主机配置・应用配置,但不会用于用户配置,以及添加自定义前缀的方法。 ↩ -
Microsoft Learn, Environment.GetFolderPath Method. 说明
Environment.SpecialFolder枚举与GetFolderPath方法获取特殊文件夹路径的方式。 ↩ -
Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. 说明
AddJsonFile的reloadOnChange参数,以及PhysicalFileProvider内部使用FileSystemWatcher监视配置文件变更的机制。 ↩ -
Microsoft Learn, Options pattern in .NET - IOptionsMonitor. 说明
IOptionsMonitor的变更通知仅限于以文件系统为基础的配置提供程序,在 Docker 容器或网络共享等变更通知不可靠的环境中,可通过DOTNET_USE_POLLING_FILE_WATCHER环境变量切换成每 4 秒一次的轮询监视。 ↩ -
Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. 说明从
App.config迁移到appsettings.json的步骤、通过System.Configuration.ConfigurationManagerNuGet 包维持兼容性,以及使用Microsoft.Extensions.Configuration.Json包的方式。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 业务应用程序的打印与 PDF 输出 ── System.Drawing.Printing / WPF / 报表库的取舍选型
本文按需求整理成判断表,梳理 PrintDocument 的 WinForms 打印、WPF 的 FlowDocument/FixedDocument 打印,以及 PDF 输出的可选方案。并从实务角度讲解分页控制、DPI 偏差等实现陷阱,以及从 Windows 服务打印、依...
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
本文说明如何用 WinDbg 与 SOS 扩展实际读取已采集的 Windows 崩溃转储。内容涵盖符号路径设置、以 !clrstack、!pe、!dumpheap -stat、!gcroot 追踪异常与内存泄漏的方法、原生崩溃的 !analyze -v,以及与 dotnet...
防止 Windows 应用程序重复启动 ── 命名 Mutex 与重复执行时的窗口前置
整理业务型 Windows 应用程序的常见需求——「不让同一个应用程序启动两次」——如何用命名 Mutex 来实现。涵盖 Global\ 与 Local\ 命名空间差异在 RDP 环境中的陷阱、拥有线程限制与 AbandonedMutexException、在 SetFor...
Windows 服务的创建与运维 ── 从任务计划程序的取舍到 BackgroundService 服务化
常驻处理应该做成 Windows 服务,还是任务计划程序就够用?本文整理判断表,以及用 .NET Worker Service(UseWindowsService)创建服务的方法、运行账户、恢复选项、事件日志、安全停止处理,从实务角度梳理投入运维所需的设计。
常见问题
汇总了咨询这一主题时常见的问题。
- appsettings.json 可以放在跟 exe 相同的文件夹里吗?
- 如果只是只读的默认值,这样做没有问题。但如果该文件夹安装在 Program Files 下面,就不能把应用设计成会自行改写 appsettings.json。标准用户没有对 Program Files 下面的写入权限,运行时不是抛出异常,就是因为 Windows 的虚拟化功能导致不同用户看到的内容不一致。需要写入的配置,应该分离到 %LOCALAPPDATA% 或 %ProgramData% 下面的另一个文件中。
- 环境变量和 appsettings.json,哪个该优先?
- 不要改变默认的优先级,直接沿用 appsettings.json → appsettings.{Environment}.json → 环境变量 → 命令行参数这种依次覆盖的机制即可。难以判断时的准则是「值的性质」:可以随构建产物一起发布的默认值放 appsettings.json,随部署对象或容器而异的值放环境变量,这样分工之后回头看也不会混淆。
- 配置类该怎么拆分?
- 基本原则是按功能或职责来拆分选项类。例如连接字符串用 ConnectionOptions、外部 API 集成用 ExternalApiOptions,避免把不相关的设置塞进同一个类。拆分之后,IOptionsSnapshot/IOptionsMonitor 的验证与重新加载单位也会自然分开,DataAnnotations 的验证也能以类为单位独立完成。
- 需要从 INI 文件或注册表迁移吗?
- 如果要重新搭建配置管理,建议统一改用 appsettings.json + IConfiguration。.NET 也提供 INI 配置提供程序,因此可以边直接读取现有的 INI 文件,边逐步迁移。注册表并不在 .NET 配置提供程序的标准支持范围内,如果有依赖注册表的现有资产,比较现实的做法是写一段只读的桥接代码,逐步把配置迁移到 appsettings.json。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。