返回 从0开始认识配置系统

(四)对于前三节的一些补充

以下为完整正文内容。

正文

命令行配置 配置系统可以从命令行参数读取配置,和读 JSON 文件一样自然。 程序启动时敲的命令,也能成为配置的一部分。 比如启动程序时这样写: dotnet run --AppName=MyApp --MaxUsers=200 --后面的内容就是命令行参数。配置系统可以识别它们,和 JSON 里的配置合并在一起用。 要启用命令行配置,只需在构建配置时加上一行 AddCommandLine(args)。和加 JSON 文件一样,施工队多加一个来源就行。 来看例子: var builder = WebApplication.CreateBuilder(args); builder.Configuration.AddCommandLine(args); args 就是 Program.cs 的 Main 方法传入的那个字符串数组,里面装的就是启动时敲的那些参数。 如果你用顶级语句(现在默认的 Program.cs 没有显式的 Main 方法),args 照样存在,直接用就行。 命令行参数的格式很灵活,支持多种主流写法。不管你怎么写,配置系统基本都能认。 三种常见写法都支持: dotnet run --AppName=MyApp dotnet run --AppName MyApp dotnet run /AppName MyApp 这三种在代码里都能用 builder.Configuration["AppName"] 取到 "MyApp"。--、空格分隔、/ 都可以,= 可有可无。 命令行配置的优先级通常比 JSON 文件高,适合临时覆盖。 你可以在启动时临时改某个配置,不改 JSON 文件。 比如 appsettings.json 里写死了 MaxUsers 是 100。今天你想临时跑一下 200 测试,就启动时敲: dotnet run --MaxUsers=200 因为命令行源后加载,它的值会覆盖 JSON 里的 100。测试完关了重开,还是 100,JSON 文件没被动过。 好,来看小练习 AddCommandLine(args) 加在 Program.cs 里之后,运行时敲的参数会永久修改 appsettings.json 文件。这句话对吗?为什么? 答案: 不会,命令行参数仅影响一次启动 好,我们继续 接下来说 配置绑定到集合 JSON 里的数组,可以自动绑定到 C# 的 List<T> 或数组类型。 不只是单对象,配置系统也能处理“一组东西”。 比如 JSON 里这样写: { "Servers": [ "北京节点", "上海节点", "广州节点" ] } 你定义的类可以这样接: public class ServerConfig { public List<string> Servers { get; set; } } 绑定方式和单对象完全一样: var config = builder.Configuration.GetSection("ServerConfig").Get<ServerConfig>(); // config.Servers[0] 就是 "北京节点" 更常用的是数组里存对象,形成一个对象列表。 配置里可以写“一张带多行的表格”,每行是一个对象。 JSON 这样写: { "Proxies": [ { "Ip": "192.168.1.1", "Port": 8080 }, { "Ip": "192.168.1.2", "Port": 8081 }, { "Ip": "192.168.1.3", "Port": 8082 } ] } 对应的 C# 类: public class ProxyItem { public string Ip { get; set; } public int Port { get; set; } } public class ProxyConfig { public List<ProxyItem> Proxies { get; set; } } 绑定后: var config = builder.Configuration.GetSection("ProxyConfig").Get<ProxyConfig>(); foreach (var proxy in config.Proxies) { Console.WriteLine($"{proxy.Ip}:{proxy.Port}"); } 三层对象列表,一行 Get<T>() 搞定。 好,继续小练习 请写出对应这段 JSON 的 C# 类定义: { "BackupServers": [ { "Address": "10.0.0.1", "Priority": 1 }, { "Address": "10.0.0.2", "Priority": 2 } ] } 答案: public class BackupServer { public string Address { get; set; } public int Priority { get; set; } } public class BackupServerConfig { public List<BackupServer> BackupServers { get; set; } } BackupServer — 单数,表示单个服务器,Address 和 Priority 对齐 JSON BackupServerConfig — 属性名 BackupServers 和 JSON 键名一致,类型是 List<BackupServer> 练习完成,我们继续 接下来是自定义配置源(数据库配置提供者) 自定义配置源的核心,是实现两个接口——IConfigurationSource 和 IConfigurationProvider。 Source 是“施工队用的图纸”,Provider 是“真正干活的工人”。 IConfigurationSource — 作用很简单,就是负责创建一个 IConfigurationProvider 实例 IConfigurationProvider — 真正干活的地方,负责从你的目标(数据库、Redis 等)把数据读出来,转成键值对,交给配置系统 我们先写 Provider 类,继承 ConfigurationProvider,重写 Load 方法。 Load 方法是核心,在这里从数据库读数据,填进 Data 字典。 框架已经提供了一个 ConfigurationProvider 基类,里面有一个 Data 属性,类型是 Dictionary<string, string>。你的任务就是在 Load 方法里,把你从数据库读到的配置,一对一对地塞进这个 Data 字典。 public class DatabaseConfigurationProvider : ConfigurationProvider { private readonly string _connectionString; public DatabaseConfigurationProvider(string connectionString) { _connectionString = connectionString; } public override void Load() { var data = new Dictionary<string, string>(); // 1. 连接数据库 // 2. 查出配置表里的所有键值对 // 3. 把查到的键值对放进 data 字典 Data = data; // 赋值给基类的 Data,配置系统就能读到 } } 构造函数里的 _connectionString 是用来连数据库的,创建 Provider 的时候传进来。 然后写 Source 类,实现 IConfigurationSource,它的唯一任务就是创建一个 Provider 实例。 Source 很简单,就是 Provider 的工厂。 public class DatabaseConfigurationSource : IConfigurationSource { private readonly string _connectionString; public DatabaseConfigurationSource(string connectionString) { _connectionString = connectionString; } public IConfigurationProvider Build(IConfigurationBuilder builder) { // 唯一的任务:new 一个 Provider,把连接字符串传进去 return new DatabaseConfigurationProvider(_connectionString); } } Build 方法就是框架调用的入口,它说“给我一个能读数据的 Provider”,你把 Provider new 出来返回就行。 在使用时,像加 JSON 文件一样,一行 Add 扩展方法把它插进去。 为了让调用方看着统一,我们通常会写一个 Add 扩展方法。(注:Add只是为了方便辨认,起任何名字都可以) public static class DatabaseConfigurationExtensions { public static IConfigurationBuilder AddDatabase( this IConfigurationBuilder builder, string connectionString) { var source = new DatabaseConfigurationSource(connectionString); builder.Add(source); return builder; } } 这样在 Program.cs 里就能像加 JSON 一样加数据库配置了: builder.Configuration.AddDatabase("你的数据库连接字符串"); 现在我们回头看 Provider 的 Load 方法,把里面“从数据库读配置”的逻辑补完整。 假设数据库里有一张配置表,结构很简单:一个键列,一个值列。 表结构假设: Key:Value AppName:MyApp MaxUsers:100 Logging:LogLevel:Default:Warning 那么 Load 方法的完整实现就是: public override void Load() { var data = new Dictionary<string, string>(); // 假设用的是 SqlConnection using (var connection = new SqlConnection(_connectionString)) { connection.Open(); using (var command = new SqlCommand("SELECT [Key], [Value] FROM Configs", connection)) using (var reader = command.ExecuteReader()) { while (reader.Read()) { var key = reader.GetString(0); // 第一列:键 var value = reader.GetString(1); // 第二列:值 data[key] = value; } } } Data = data; } 做的事很简单:查出所有行,遍历每一行,键值对塞进 Data 字典。配置系统就能用 builder.Configuration["键"] 读到数据库里的值了。 全注释版: public override void Load() { // 创建一个空字典,准备装从数据库读出来的配置 var data = new Dictionary<string, string>(); // 用构造函数传进来的连接字符串,创建一个数据库连接 // SqlConnection 是用来连接 SQL Server 数据库的类 using (var connection = new SqlConnection(_connectionString)) { // 打开数据库连接 connection.Open(); // 创建一条 SQL 命令,告诉数据库“把 Configs 表里所有行都给我” // [Key] 和 [Value] 是表里的两个列名 using (var command = new SqlCommand("SELECT [Key], [Value] FROM Configs", connection)) // 执行命令,拿到一个“结果读取器” reader // reader 里装的就是数据库返回的所有行 using (var reader = command.ExecuteReader()) { // 一行一行地读,Read() 返回 true 表示还有下一行 while (reader.Read()) { // 从当前行里取出第一列的值(Key列),转为 string // GetString(0) 意思是拿第 0 个字段(第一列) var key = reader.GetString(0); // 从当前行里取出第二列的值(Value列),转为 string // GetString(1) 意思是拿第 1 个字段(第二列) var value = reader.GetString(1); // 把键和值放进字典 // 比如 key = "AppName", value = "MyApp" data[key] = value; } } } // 这里 using 结束,数据库连接自动关闭 // 把装满配置的字典交给基类的 Data 属性 // 配置系统就能通过这个 Data 读到所有配置了 Data = data; } 自定义配置源也支持 reloadOnChange 自动刷新,但需要自己实现重读逻辑。 总结: 文件和数据库不一样,数据库没有“文件修改时间”可以监控,所以要实现定时或主动刷新。 做法是:在 Provider 里加一个定时器,每隔一段时间重新执行一次 Load 里的查询,更新 Data。Data 一更新,配置系统会自动感知。 骨架思路: ```csharp // 构造函数里启动定时器 public DatabaseConfigurationProvider(string connectionString) { _connectionString = connectionString; // 每隔 30 秒重新执行 Load 方法 var timer = new Timer(_ => Load(), null, TimeSpan.Zero, TimeSpan.FromSeconds(30)); } ``` 注意 Load 方法要写成能被反复调用的形式(每次清空重新查)。框架发现 Data 变了,会自动通知所有使用配置的地方。 这一点比文件监控复杂一些,但原理相通:数据变了就重读,框架负责通知。 本节小结 命令行配置: AddCommandLine(args) 一行启用,多种格式支持,只影响当次运行 绑定到集合: JSON 数组自动映射到 List<T>,数组里可以是简单值也可以是对象 自定义配置源: 实现 IConfigurationSource(工厂)和 IConfigurationProvider(工人),重写 Load 读数据填 Data Add 扩展方法: 命名约定,让调用方和框架风格统一 reloadOnChange 原理: 文件源靠文件监控,数据库源靠定时器反复执行 Load