不只是 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.jsonappsettings.{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

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. Secret Manager(仅开发环境时)
  4. 环境变量
  5. 命令行参数

无论是控制台应用、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_ENVIRONMENTASPNETCORE_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 一个 HttpClientDispose,就会每次都废弃、重建内部的套接字与连接池,在高频率轮询或批处理中容易导致临时端口(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.ProtectedDataProtect/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 保护的值该存在哪里、CurrentUserLocalMachine 该选哪个作用域、多个用户共用同一台机器的业务应用会遇到哪些陷阱,这类实现层级的细节写在「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 迁移的方针咨询,往往需要实际看过配置文件和部署环境才能找到最佳解,如果犹豫不决,欢迎与我们洽谈。

相关文章

相关咨询领域

合同会社小村软件处理 Windows 业务应用的配置管理设计、环境差异化设置的运维设计,以及现有 app.config 资产迁移方针的技术咨询。

参考链接

  1. Microsoft Learn, Configuration in .NET - Alternative hosting approach. 说明 Host.CreateApplicationBuilder 默认组成的配置提供程序优先级(命令行参数 → 环境变量 → 开发环境的 user secrets → appsettings.{Environment}.json → appsettings.json)。  2 3

  2. Microsoft Learn, .NET Generic Host - Host builder settings. 说明 Host.CreateApplicationBuilder 读取的主机配置(带 DOTNET_ 前缀的环境变量、命令行参数)与应用配置(appsettings.json、appsettings.{Environment}.json、Secret Manager、环境变量、命令行参数)的默认顺序。  2

  3. Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. 说明在 Windows Forms 应用中集成 Generic Host、使用 DI・配置・日志的步骤。 

  4. Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. 说明 DOTNET_ENVIRONMENTASPNETCORE_ENVIRONMENT 的关系、使用 WebApplicationDOTNET_ENVIRONMENT 优先、未设置时默认值为 Production,以及 Windows 上环境变量名称不区分大小写、但 Linux 上会区分。  2

  5. Microsoft Learn, Options pattern in .NET - Options interfaces. 说明 IOptions<TOptions>IOptionsSnapshot<TOptions>IOptionsMonitor<TOptions> 在生命周期、配置变更反映时机、对应功能上的差异。  2

  6. Microsoft Learn, Options pattern in .NET - Options validation. 说明通过 ValidateDataAnnotations() 进行 DataAnnotations 验证,以及通过 ValidateOnStart()(或 AddOptionsWithValidateOnStart)进行启动时验证的设置方式。  2 3

  7. 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

  8. Microsoft Learn, ProtectedData Class. 说明包装 DPAPI(Data Protection API)的 ProtectedData.Protect/Unprotect 方法、DataProtectionScope.CurrentUser/LocalMachine 作用域的差异,以及该功能仅限 Windows、在其他平台会抛出 PlatformNotSupportedException。  2

  9. Microsoft Learn, Configuration providers in .NET. 说明将 JSON・环境变量・命令行・INI・XML 等性质不同的配置来源整合到 IConfiguration 之后的配置提供程序机制。 

  10. Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. 说明默认配置会将带 DOTNET_ 前缀的环境变量・命令行参数读入主机配置・应用配置,但不会用于用户配置,以及添加自定义前缀的方法。 

  11. Microsoft Learn, Environment.GetFolderPath Method. 说明 Environment.SpecialFolder 枚举与 GetFolderPath 方法获取特殊文件夹路径的方式。 

  12. Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. 说明 AddJsonFilereloadOnChange 参数,以及 PhysicalFileProvider 内部使用 FileSystemWatcher 监视配置文件变更的机制。 

  13. Microsoft Learn, Options pattern in .NET - IOptionsMonitor. 说明 IOptionsMonitor 的变更通知仅限于以文件系统为基础的配置提供程序,在 Docker 容器或网络共享等变更通知不可靠的环境中,可通过 DOTNET_USE_POLLING_FILE_WATCHER 环境变量切换成每 4 秒一次的轮询监视。 

  14. Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. 说明从 App.config 迁移到 appsettings.json 的步骤、通过 System.Configuration.ConfigurationManager NuGet 包维持兼容性,以及使用 Microsoft.Extensions.Configuration.Json 包的方式。  2

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表