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

(二)强类型绑定与 Options 模式

以下为完整正文内容。

正文

强类型绑定的核心思想,是把 JSON 里的一个“段”,直接映射成一个我们自己定义的 C# 类对象。 配置不再是一个一个取值,而是整段“翻译”成一个对象。 在第一节,我们要取 GameSettings 下的 Title、MaxPlayers、EnableSound,得写三行代码,一个一个按字符串键去拿,还要自己转换类型。 强类型绑定做的是:定义一个 C# 类,类的属性名和 JSON 里的键名一一对应,然后配置系统自动帮你把整个段的值填进去,生成一个对象。你拿到这个对象后,直接用 .Title、.MaxPlayers 这种点号访问,有智能提示,类型也对。 要实现绑定,第一步是定义一个和 JSON 段结构匹配的 C# 类。 JSON 里的键,变成类的属性;JSON 段的结构,变成类的嵌套。 什么意思?叽里咕噜说什么呢看看例子 拿我们之前的 JSON 举例: { "GameSettings": { "Title": "Space Adventure", "MaxPlayers": 4, "EnableSound": true } } 对应的 C# 类这样写: public class GameSettings { public string Title { get; set; } public int MaxPlayers { get; set; } public bool EnableSound { get; set; } } 注意三个属性名 Title、MaxPlayers、EnableSound 和 JSON 里的键一模一样。名字对不上就没法自动映射。类型也直接写成对应的 string、int、bool,不需要你手动转换。 第二步,用 GetSection 拿到那个段,然后调用 Get<T>() 方法把它转成你定义的类的对象。 人话:定位段之后,加个 .Get<你的类名>() 就完成绑定。 写法很简单: var gameSettings = builder.Configuration.GetSection("GameSettings").Get<GameSettings>(); 这一行做了三件事: 1. GetSection("GameSettings") — 定位到 JSON 里的那个段 2. .Get<GameSettings>() — 把段里的值填进 GameSettings 类,生成一个对象 3. 返回的对象赋给变量 gameSettings 现在你就可以这样用了: Console.WriteLine(gameSettings.Title); // 直接点出来,有智能提示 int total = gameSettings.MaxPlayers + 2; // 已经是 int,不需要转换 对比第一节先取再转换,是不是简洁了很多? 这种手动 Get<T>() 的方式适合在启动阶段临时用,在项目中更推荐的是 Options 模式,让它自动注入到你需要的任何地方。 Get<T>() 是一次性取出,Options 模式是“注册到系统中,随用随取”。 手动 Get<T>() 有个局限:你只能在 Program.cs 里调用,拿到对象后还得想办法传给要用它的服务。项目一大,这样很麻烦。 Options 模式做的是:在启动时把配置“注册”到依赖注入容器里,之后任何控制器、服务只要在构造函数里声明要用它,系统就自动把配置对象递过来。这更解耦、更自动化。 等等,什么东西这那的?看看例子 好,我们对着代码来理解 手动 Get<T>(): 你只能在 Program.cs 里写这一行: var gameSettings = builder.Configuration.GetSection("GameSettings").Get<GameSettings>(); 现在 gameSettings 这个对象就在 Program.cs 这个文件里。如果另一个文件里的 GameService 要用它,你必须想办法传过去,比如: var service = new GameService(gameSettings); 现在项目里有1000个服务都这么手动传,代码会很乱,维护起来很费人。 Options 模式对于初见者比较难,我们慢慢看: Options 模式的第一步,是在 Program.cs 里用 builder.Services.Configure<T>() 把配置段注册到系统中。 builder.Services.Configure<GameSettings>(builder.Configuration.GetSection("GameSettings")); 太长了不好读,我们分成小段来理解 builder.Services //系统的“服务仓库” .Configure<GameSettings>(...) //往仓库里存一个 GameSettings 类型的配置 builder.Configuration.GetSection("GameSettings") // 告诉它数据来源是 JSON 里的哪个段 这一行代码的意思是“把这个配置段交给系统保管,以后谁要就给它”。 执行完这行,系统就知道:以后谁需要 GameSettings 配置,就把这个段的内容给它。 注册完之后,在任何需要使用配置的类里,通过构造函数注入 IOptions<T>,系统就会自动把配置对象递进来 public class GameService { private readonly GameSettings _settings; public GameService(IOptions<GameSettings> options) { _settings = options.Value; } } 系统做的事:当其他地方需要 GameService 时,系统先看到构造函数要 IOptions<GameSettings>,就去仓库里找到之前注册的配置段,自动填进去。你全程不用手动传参。 如果看明白了,那么接下来做个小练习 // Program.cs builder.Services.Configure<GameSettings>(builder.Configuration.GetSection("GameSettings")); // appsettings.json 中 { "GameSettings": { "Title": "Space Adventure" } } // GameService 中 public GameService(IOptions<GameSettings> options) { _settings = options.Value; Console.WriteLine(_settings.Title); } 看上面的代码,最后 _settings.Title 的值是什么? 答案: Space Adventure 详解: builder.Services.Configure<GameSettings>(builder.Configuration.GetSection("GameSettings")); 这行把 JSON 里 GameSettings 段的数据注册到系统仓库。仓库里现在存了一个东西:钥匙是 IOptions<GameSettings>,内容是 GameSettings 段的数据 当系统发现有人需要 GameService,它会先看构造函数: public GameService(IOptions<GameSettings> options) 系统说:“这个构造函数要 IOptions<GameSettings>,我已经注册过,直接给他。” 于是系统从 JSON 读数据,生成一个 GameSettings 对象: Title = "Space Adventure" 然后把它放进一个 IOptions<GameSettings> 包装里,递给构造函数。 _settings = options.Value; Console.WriteLine(_settings.Title); options.Value 就是刚才生成的 GameSettings 对象。_settings.Title 访问的就是该对象的 Title 属性,值为 "Space Adventure"。 确保理解该练习后我们继续 Options 模式有三种接口来接收配置,区别在于配文件修改后,配置值会不会自动更新。 IOptions、IOptionsSnapshot、IOptionsMonitor IOptions<T> 程序启动后就不再变,永远是第一次读到的值 配置不改的静态设置 IOptionsSnapshot<T> 每次请求重新读一次,请求内不变 配置可能改,但请求内要一致 IOptionsMonitor<T> 配文件一改立刻生效,实时更新 需要随时感知变化的场景 知道概念了,我们在代码里看看是什么样的 我们拿同一个 appsettings.json 举例: { "GameSettings": { "Title": "Space Adventure", "MaxPlayers": 4 } } IOptions<T> —— 单例,终身不变: public class GameService { private readonly GameSettings _settings; public GameService(IOptions<GameSettings> options) { _settings = options.Value; } } 程序启动读到 MaxPlayers = 4。运行期间你把 JSON 改成 MaxPlayers = 8,这里读到的还是 4,除非重启程序。 IOptionsSnapshot<T> —— 每个请求刷新一次: public class GameService { private readonly GameSettings _settings; public GameService(IOptionsSnapshot<GameSettings> options) { _settings = options.Value; } } ``` 用户第一次访问,读到 MaxPlayers = 4。你在后台把 JSON 改成 8。同一个用户刷新页面,新请求进来,读到的就是 8 IOptionsMonitor<T> —— 实时推送变化: public class GameService { private readonly IOptionsMonitor<GameSettings> _monitor; public GameService(IOptionsMonitor<GameSettings> monitor) { _monitor = monitor; } public void DoSomething() { var current = _monitor.CurrentValue; // 每次访问都是最新值 Console.WriteLine(current.MaxPlayers); } } ``` 你 JSON 一保存,CurrentValue 立刻是新的 8,不需要等新请求。还能注册回调监听变化: _monitor.OnChange(newSettings => { Console.WriteLine($"配置变了!新值:{newSettings.MaxPlayers}"); }); 看懂后,我们进行小练习: “如果希望 appsettings.json 修改后,程序不重启就能读到新配置,应该使用 IOptions<T>。” 这句话对吗?为什么? 答案: ✖ IOptions<T> 是终身不变;IOptionsSnapshot<T> 是新请求触发刷新;IOptionsMonitor<T> 是文件一改立刻感知,连新请求都不用等。 好我们继续 Options 模式配合 ValidateOnStart 可以在程序启动时就校验配置是否合法,避免运行到一半才发现配置缺失。 人话:启动时先检查一遍配置,不合格直接拒绝启动,而不是跑到一半崩溃。 写法只需要在注册时链式调用: builder.Services.AddOptions<GameSettings>() .Bind(builder.Configuration.GetSection("GameSettings")) .ValidateDataAnnotations() .ValidateOnStart(); 哎呀有点复杂了,这都哪跟哪啊? 不慌,依旧耐下心一点点看 builder.Services.AddOptions<GameSettings>() 在服务仓库里,开启对 GameSettings 这个配置类的登记流程。 之前我们用 builder.Services.Configure<T>() 一步完成注册。现在用 AddOptions<T>() 是另一种启动方式,它返回一个“配置构建器”对象,让你可以继续往后面加东西,比如绑定数据源、加校验等。这是一个链式调用的起点。 .Bind(builder.Configuration.GetSection("GameSettings")) 告诉构建器,数据从 JSON 的 GameSettings 段来。 这和之前 Configure 里传配置段是一个意思,只是现在分开写了。 Bind 就是把 JSON 数据源和 GameSettings 类绑在一起。 .ValidateDataAnnotations() 开启数据注解校验,启动时检查类上的 [Required]、[Range] 等特性。 比如类: public class GameSettings { [Required] public string Title { get; set; } [Range(1, 100)] public int MaxPlayers { get; set; } } 加了这一行后,系统会用这些 [ ] 标签规则去检查配置值是否合规。 .ValidateOnStart() 让校验发生在程序启动时,而不是等到第一次使用时。 如果 JSON 里 Title 没配,或者 MaxPlayers 写了 0 或 200,程序启动时直接抛异常,告诉你哪里配错了,不会等到用户点了按钮才发现。 如果不写这行,校验只在某个服务第一次注入 IOptions<T> 时触发。如果这个服务1小时后才被访问,配置错了就得在1小时后才暴露。加上这行,程序一启动立刻校验,配错当场报错,第一时间就知道。 解释完毕,再回过头来看一遍无注解版 builder.Services.AddOptions<GameSettings>() .Bind(builder.Configuration.GetSection("GameSettings")) .ValidateDataAnnotations() .ValidateOnStart(); public class GameSettings { [Required] public string Title { get; set; } [Range(1, 100)] public int MaxPlayers { get; set; } } 是不是感觉清晰很多了 恭喜你,坚持到了最后。本节已接近尾声,接下来是小总结与BOSS战 强类型绑定: JSON 段 → C# 类对象,属性名一一对应 手动绑定: GetSection("段").Get<类>(),适合启动阶段一次性取 Options注册: builder.Services.Configure<T>(配置段) 存入系统仓库 Options 接收: 构造函数注入 IOptions<T> 等,系统自动递过来 三种接口: IOptions 不变、IOptionsSnapshot 请求级刷新、IOptionsMonitor 实时 启动校验: ValidateOnStart() + 数据注解,配错立即报 如果准备好了,就开始本节的最终考验 { "GameSettings": { "Title": "Space Adventure", "MaxPlayers": 4, "EnableSound": true } } 题1: 写出 GameSettings 这个 C# 类,属性名和类型都要对。 题2: 写出 Program.cs 中注册 Options 模式的代码。 题3: 写出 GameService 类的构造方法,要求每个请求都能读到最新配置。 题4: 判断题——IOptionsSnapshot<T> 拿到配置后,如果 JSON 文件在请求处理中途被修改了,当前请求读到的值会立刻变。这句话对吗?为什么? 答案; (1) class GameSettings { public string Title{ get ; set ;} public int MaxPlayers{ get ; set ;} public bool EnableSound { get ; set ;} } (2) builder.Services.AddOptions<GameSettings>() .Bind(builder.Configuration.GetSection("GameSettings")) .ValidateDataAnnotations() .ValidateOnStart(); (3) public class GameService { private readonly IOptionsMonitor<GameSettings> _monitor; public GameService(IOptionsMonitor<GameSettings> monitor) { _monitor = monitor; } 或者 public class GameService { private readonly GameSettings _settings; public GameService(IOptionsSnapshot<GameSettings> options) { _settings = options.Value; } } (4) 不会 snapshot仅在下一次请求才更新 如果你独立答对了,恭喜你,本节已经毕业啦 你现在至少已经掌握了这些: 会定义配置类,属性名和类型对齐 JSON 会注册 Options 并注入到服务中 会根据场景选对接口:静态用 IOptions,请求级刷新用 IOptionsSnapshot,实时监听用 IOptionsMonitor 会加启动校验,防止配置错误流入运行期 下期预告: (三)多环境管理与安全实践