Day 3 - 配置管理
建议用时:240-300 分钟
你将学会什么
- 什么是配置,为什么不要把可变值写死在代码里
appsettings.json通常放什么- 如何用配置类承载配置
- 如何从 JSON 文件读取配置
- 如何给配置设置默认值和校验规则
- 如何用环境变量覆盖配置
- 为什么密码、Token、连接字符串不能随便提交到源码里
配置管理的核心是:会变化的值离开代码,业务代码只依赖配置对象。路径、开关、超时时间、接口地址、日志级别都属于配置。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“依赖注入”,今天继续学习“配置管理”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“配置管理”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。配置管理不是背 appsettings.json,而是知道哪些值应该离开代码。
1. 什么是配置
配置就是程序运行时需要用到、但可能会变化的参数。
常见配置:
| 配置 | 例子 |
|---|---|
| 文件路径 | products.txt |
| 接口地址 | https://api.example.com |
| 超时时间 | 10 秒 |
| 功能开关 | EnableLog = true |
| 日志级别 | Information、Warning |
| 连接字符串 | 数据库地址、账号信息 |
2. 为什么不要写死在代码里
如果到处写:
"products.txt"
10
"https://api.example.com"会出现问题:
- 改配置要改代码。
- 多个地方可能改漏。
- 开发、测试、生产很难用不同配置。
- 密码、Token 可能被提交到代码仓库。
配置文件的作用是把变化集中管理。
3. 什么是 appsettings.json
appsettings.json 是 .NET 项目里常见的配置文件名。
它通常长这样:
{
"DataFile": "products.txt",
"EnableLog": true,
"TimeoutSeconds": 10
}文件名不是魔法。真正重要的是:
配置放在代码外面,程序启动时读取。4. 什么是配置类
配置类是用来承载配置的类。
例如:
AppOptions它的属性对应配置项:
DataFile
EnableLog
TimeoutSeconds好处是:业务代码不用到处拿字符串 key,而是使用强类型属性。
5. 什么是 Options 模式
Options 模式就是把配置绑定成一个类,然后把这个类交给需要它的服务。
简化理解:
JSON 配置 -> AppOptions 对象 -> ProductService 使用 AppOptions真实框架里经常会看到 IOptions<AppOptions>。本页先用普通 AppOptions 对象理解这个思想。
6. 默认值有什么用
配置可能缺失。
配置类可以给默认值:
TimeoutSeconds = 5
EnableLog = false如果 JSON 里没写这些字段,就使用默认值。
默认值不能替代校验。关键配置仍然要检查。
7. 配置校验是什么
配置校验就是程序启动时检查配置是否合理。
例如:
- 文件路径不能为空。
- 超时时间必须大于 0。
- 接口地址必须是合法 URL。
- 连接字符串不能缺失。
越早发现配置错误,越容易定位问题。
8. 环境变量是什么
环境变量是操作系统提供的配置来源。
它适合放:
- 不同环境的配置。
- 部署平台传入的参数。
- 不想写进文件的敏感值。
C# 读取环境变量:
Environment.GetEnvironmentVariable("PRODUCT_DATA_FILE")9. 敏感配置怎么处理
敏感配置包括:
- 数据库密码。
- API Token。
- 私钥。
- 支付密钥。
- 短信平台密钥。
不要把这些写进公开仓库。
练习项目可以写假值,但真实项目要使用环境变量、用户机密或部署平台的密钥管理。
配置管理常用 API 速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 创建配置构建器 | new ConfigurationBuilder() | 配置入口 |
| 设置基础路径 | .SetBasePath(...) | 找配置文件的位置 |
| 加载 JSON | .AddJsonFile("appsettings.json") | 常见配置文件 |
| 加载环境变量 | .AddEnvironmentVariables() | 部署时覆盖配置 |
| 构建配置 | .Build() | 得到 IConfiguration |
| 读取字符串 | config["App:Name"] | 用冒号读层级 |
| 读取数字 | config.GetValue<int>("App:Port") | 转成指定类型 |
| 绑定对象 | config.GetSection("App").Get<AppOptions>() | 配置转对象 |
配置文件例子:
{
"App": {
"Name": "ProductApp",
"MaxItems": 100
}
}读取例子:
string? name = configuration["App:Name"];
int maxItems = configuration.GetValue<int>("App:MaxItems");配置适合放会变化的值,不适合放业务规则代码。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要能从 JSON 文件读取配置,并把配置传给服务。
先看效果:读取 appsettings.json
using System.Text.Json;
string path = "appsettings.json";
await File.WriteAllTextAsync(path, """
{
"DataFile": "products.txt",
"EnableLog": true,
"TimeoutSeconds": 10
}
""");
string json = await File.ReadAllTextAsync(path);
AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions();
options.Validate();
ProductService service = new ProductService(options);
service.PrintSettings();
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; } = false;
public int TimeoutSeconds { get; set; } = 5;
public void Validate()
{
if (string.IsNullOrWhiteSpace(DataFile))
{
throw new InvalidOperationException("DataFile 配置不能为空");
}
if (TimeoutSeconds <= 0)
{
throw new InvalidOperationException("TimeoutSeconds 必须大于 0");
}
}
}
class ProductService
{
private readonly AppOptions _options;
public ProductService(AppOptions options)
{
_options = options;
}
public void PrintSettings()
{
Console.WriteLine($"数据文件: {_options.DataFile}");
Console.WriteLine($"启用日志: {_options.EnableLog}");
Console.WriteLine($"超时时间: {_options.TimeoutSeconds} 秒");
}
}这个例子完整做了四件事:
- 准备配置文件。
- 读取 JSON。
- 反序列化成配置对象。
- 把配置对象传给服务使用。
第三部分:跟着敲代码
从这里开始动手。每个例子都是完整代码,可以直接放进 Program.cs 运行。
动手前先做这 3 件事
- 打开一个控制台项目。
- 每次只保留一个例子的代码,运行通过后再换下一个。
- 每个例子都改一次配置值,再运行观察输出变化。
例子 1:硬编码的问题
string dataFile = "products.txt";
bool enableLog = true;
int timeoutSeconds = 10;
Console.WriteLine($"数据文件: {dataFile}");
Console.WriteLine($"启用日志: {enableLog}");
Console.WriteLine($"超时时间: {timeoutSeconds}");这个例子能跑,但配置散落在代码里。项目变大后不好管理。
例子 2:集中到配置对象
AppOptions options = new AppOptions
{
DataFile = "products.txt",
EnableLog = true,
TimeoutSeconds = 10
};
Console.WriteLine($"数据文件: {options.DataFile}");
Console.WriteLine($"启用日志: {options.EnableLog}");
Console.WriteLine($"超时时间: {options.TimeoutSeconds}");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; }
public int TimeoutSeconds { get; set; } = 5;
}先把散落的配置集中成对象,这是 Options 模式的基础。
例子 3:写入配置文件
string json = """
{
"DataFile": "products.txt",
"EnableLog": true,
"TimeoutSeconds": 10
}
""";
await File.WriteAllTextAsync("appsettings.json", json);
Console.WriteLine("配置文件已写入");运行后会生成 appsettings.json。
例子 4:从 JSON 读取配置对象
using System.Text.Json;
await File.WriteAllTextAsync("appsettings.json", """
{
"DataFile": "products.txt",
"EnableLog": true,
"TimeoutSeconds": 10
}
""");
string json = await File.ReadAllTextAsync("appsettings.json");
AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions();
Console.WriteLine($"数据文件: {options.DataFile}");
Console.WriteLine($"启用日志: {options.EnableLog}");
Console.WriteLine($"超时时间: {options.TimeoutSeconds}");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; }
public int TimeoutSeconds { get; set; } = 5;
}JsonSerializer.Deserialize<AppOptions> 会把 JSON 转成 AppOptions 对象。
例子 5:配置默认值
using System.Text.Json;
await File.WriteAllTextAsync("appsettings.json", """
{
"DataFile": "products.txt"
}
""");
string json = await File.ReadAllTextAsync("appsettings.json");
AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions();
Console.WriteLine($"数据文件: {options.DataFile}");
Console.WriteLine($"启用日志: {options.EnableLog}");
Console.WriteLine($"超时时间: {options.TimeoutSeconds}");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; } = false;
public int TimeoutSeconds { get; set; } = 5;
}JSON 只写了 DataFile,其他属性会使用类里的默认值。
例子 6:配置校验
AppOptions options = new AppOptions
{
DataFile = "",
TimeoutSeconds = 0
};
try
{
options.Validate();
Console.WriteLine("配置有效");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"配置错误: {ex.Message}");
}
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public int TimeoutSeconds { get; set; } = 5;
public void Validate()
{
if (string.IsNullOrWhiteSpace(DataFile))
{
throw new InvalidOperationException("DataFile 不能为空");
}
if (TimeoutSeconds <= 0)
{
throw new InvalidOperationException("TimeoutSeconds 必须大于 0");
}
}
}配置错误应该尽早暴露,而不是等业务运行到一半才失败。
例子 7:服务使用配置对象
AppOptions options = new AppOptions
{
DataFile = "products.txt",
EnableLog = true
};
ProductService service = new ProductService(options);
service.Save("键盘");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; }
}
class ProductService
{
private readonly AppOptions _options;
public ProductService(AppOptions options)
{
_options = options;
}
public void Save(string name)
{
File.AppendAllText(_options.DataFile, name + Environment.NewLine);
if (_options.EnableLog)
{
Console.WriteLine($"已保存商品: {name}");
}
}
}业务服务不再写死文件路径,而是从配置对象里拿。
例子 8:环境变量覆盖配置
AppOptions options = new AppOptions
{
DataFile = "products.txt"
};
Environment.SetEnvironmentVariable("PRODUCT_DATA_FILE", "products-dev.txt");
string? dataFileFromEnv = Environment.GetEnvironmentVariable("PRODUCT_DATA_FILE");
if (!string.IsNullOrWhiteSpace(dataFileFromEnv))
{
options.DataFile = dataFileFromEnv;
}
Console.WriteLine($"最终数据文件: {options.DataFile}");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
}环境变量可以覆盖配置文件里的值,适合不同运行环境。
例子 9:嵌套配置
using System.Text.Json;
await File.WriteAllTextAsync("appsettings.json", """
{
"Api": {
"BaseUrl": "https://api.example.com",
"TimeoutSeconds": 10
},
"Feature": {
"EnableLog": true
}
}
""");
string json = await File.ReadAllTextAsync("appsettings.json");
AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions();
Console.WriteLine(options.Api.BaseUrl);
Console.WriteLine(options.Api.TimeoutSeconds);
Console.WriteLine(options.Feature.EnableLog);
class AppOptions
{
public ApiOptions Api { get; set; } = new ApiOptions();
public FeatureOptions Feature { get; set; } = new FeatureOptions();
}
class ApiOptions
{
public string BaseUrl { get; set; } = "";
public int TimeoutSeconds { get; set; } = 5;
}
class FeatureOptions
{
public bool EnableLog { get; set; }
}配置多了以后,可以按模块拆成嵌套对象。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 配置值写死在代码里 | 环境变化要改代码 | 放进 appsettings.json 或环境变量 |
| 配置文件没复制到输出目录 | 运行时读不到文件 | 检查 .csproj 的复制设置 |
| key 名拼错 | 读取到默认值或空值 | 集中定义配置路径或使用 Options 类 |
| 数字配置不校验 | 错误配置会导致运行异常 | 读取后检查范围 |
| 敏感信息提交到仓库 | 有安全风险 | 密码、密钥用环境变量或安全存储 |
小白重复敲写训练
配置训练要同时改配置文件和读取代码。
训练 1:最小 JSON 配置
{
"AppName": "Product Manager",
"PageSize": 20
}手敲到 appsettings.json,故意漏一个逗号,观察 JSON 错误后再修复。
训练 2:读取配置值
using Microsoft.Extensions.Configuration;
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Console.WriteLine(config["AppName"]);
Console.WriteLine(config.GetValue<int>("PageSize"));改动任务:增加 DataFile 配置并读取。
训练 3:绑定成对象
var options = new AppOptions();
config.Bind(options);
Console.WriteLine($"{options.AppName} - {options.PageSize}");
class AppOptions
{
public string AppName { get; set; } = "";
public int PageSize { get; set; }
}第二遍把属性名故意写错,观察为什么拿不到值。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:配置管理。今天至少要掌握的 3 个点是:
1. 什么是配置,为什么不要把可变值写死在代码里
2. `appsettings.json` 通常放什么
3. 如何用配置类承载配置答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:什么是配置,为什么不要把可变值写死在代码里。
上位机专项练习
设备地址、采集周期和报警阈值应该放进配置,不应该散落在代码里。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:appsettings.json 设备配置
{
"Device": {
"Name": "PLC-01",
"IpAddress": "192.168.1.10",
"PollInterval": 1000
}
}运行结果或界面效果:
修改配置即可更换设备地址改动任务: 把采集周期改成 500。
专项例子 2:读取配置对象
class DeviceOptions
{
public string Name { get; set; } = "";
public int PollInterval { get; set; }
}
DeviceOptions options = configuration.GetSection("Device").Get<DeviceOptions>()!;
Console.WriteLine($"{options.Name}: {options.PollInterval} ms");运行结果或界面效果:
PLC-01: 1000 ms改动任务: 增加 IpAddress 并输出。
专项例子 3:检查错误配置
int interval = 100;
if (interval < 200)
{
Console.WriteLine("采集周期不能小于 200 ms");
}
else
{
Console.WriteLine("配置有效");
}运行结果或界面效果:
采集周期不能小于 200 ms改动任务: 测试 200 和 1000。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。建议先照着敲一遍,再修改 JSON 里的配置值验证。
作业 1:读取基础配置
要求:
- 写入
appsettings.json。 - 配置包含
DataFile、EnableLog、TimeoutSeconds。 - 读取并转成
AppOptions。 - 输出配置值。
完整答案
using System.Text.Json;
await File.WriteAllTextAsync("appsettings.json", """
{
"DataFile": "products.txt",
"EnableLog": true,
"TimeoutSeconds": 10
}
""");
string json = await File.ReadAllTextAsync("appsettings.json");
AppOptions options = JsonSerializer.Deserialize<AppOptions>(json) ?? new AppOptions();
Console.WriteLine($"数据文件: {options.DataFile}");
Console.WriteLine($"启用日志: {options.EnableLog}");
Console.WriteLine($"超时时间: {options.TimeoutSeconds}");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; }
public int TimeoutSeconds { get; set; } = 5;
}作业 2:配置校验
要求:
DataFile不能为空。TimeoutSeconds必须大于 0。- 配置错误时输出错误提示。
完整答案
AppOptions options = new AppOptions
{
DataFile = "",
TimeoutSeconds = 0
};
try
{
options.Validate();
Console.WriteLine("配置正确");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"配置错误: {ex.Message}");
}
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public int TimeoutSeconds { get; set; } = 5;
public void Validate()
{
if (string.IsNullOrWhiteSpace(DataFile))
{
throw new InvalidOperationException("DataFile 不能为空");
}
if (TimeoutSeconds <= 0)
{
throw new InvalidOperationException("TimeoutSeconds 必须大于 0");
}
}
}作业 3:服务使用配置
要求:
ProductService通过构造函数接收AppOptions。- 保存商品时使用
DataFile。 EnableLog为 true 时输出日志。
完整答案
AppOptions options = new AppOptions
{
DataFile = "products.txt",
EnableLog = true
};
ProductService service = new ProductService(options);
service.Save("键盘");
service.Save("鼠标");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
public bool EnableLog { get; set; }
}
class ProductService
{
private readonly AppOptions _options;
public ProductService(AppOptions options)
{
_options = options;
}
public void Save(string name)
{
File.AppendAllText(_options.DataFile, name + Environment.NewLine);
if (_options.EnableLog)
{
Console.WriteLine($"保存商品: {name}");
}
}
}作业 4:环境变量覆盖配置
要求:
- 默认
DataFile是products.txt。 - 设置环境变量
PRODUCT_DATA_FILE。 - 如果环境变量有值,就覆盖默认配置。
完整答案
AppOptions options = new AppOptions
{
DataFile = "products.txt"
};
Environment.SetEnvironmentVariable("PRODUCT_DATA_FILE", "products-local.txt");
string? fromEnv = Environment.GetEnvironmentVariable("PRODUCT_DATA_FILE");
if (!string.IsNullOrWhiteSpace(fromEnv))
{
options.DataFile = fromEnv;
}
Console.WriteLine($"最终 DataFile: {options.DataFile}");
class AppOptions
{
public string DataFile { get; set; } = "data.txt";
}本页最后要记住
- 配置是会变化的运行参数。
- 会变化的值不要散落在代码里。
appsettings.json是常见配置文件。- 配置类让配置变成强类型对象。
- 默认值能处理缺失配置,但关键配置还要校验。
- 环境变量适合覆盖不同环境的配置。
- 密码、Token、真实连接字符串不要提交到源码里。