前言
选项模式 Options 是Dotnet非常重要的一个基础概念,在应用开发过程中很多Service都关联着其 Options。
我们有个AI Agent使用 Options 来配置AI的一部分功能,原需求是只需要支持英文语言,现需求改为要支持其它共6种语言。我决定开发一个类库,使 Options 完整地得到多语言支持。
设计思路
1 具体语言的Configuration
使用OptionsLocalization:{OptionsName}:{culture}做为配置的key前缀,例如AIOptions选项的ModelId属性,在配置里的zh-CN对应的key是OptionsLocalization:AIOptions:zh-CN:ModelId
2 简化的 json 配置文件
未简化前的 AIOptions/zh-CN.json
{ "OptionsLocalization": { "AIOptions": { "zh-CN": { "ModelId": "gemini2.5", "Prompt": "你好世界" } } } }
期待简化后的 AIOptions/zh-CN.json
{ "ModelId": "gemini2.5", "Prompt": "你好世界" }
3 语言区域别名化的 Options
// 选项绑定到别名化的配置 // 如果是默认语言区域,则注册成别名为Options.DefaultName services.Configure<AIOptions>("zh-CN", configuration.GetSection("OptionsLocalization:AIOptions:zh-CN"));
// 使用别名获取选项 IOptionsMonitor<AIOptions>().Get("zh-CN");
4 支持父语言区域回退
假设注册"en"默认语言和zh语言
services.Configure<AIOptions>("zh", zhSection); services.Configure<AIOptions>(Options.DefaultName, enSection);
现在前端的语言区域为"zh-CN",IOptionsMonitor<AIOptions>().Get("zh-CN")会存在以下问题:
zh-CN不存在,要回退到zh-Hanszh-Hans不存在,要回退到zhzh下的ModelId没有配置项,要回退使用默认的en下的ModelId项
我们需要实现自定义的IOptionsFactory<TOptions>,把指定的语言区域别名的AIOptions构建正确。
sealed class CultureOptionsFactory<TOptions> : IOptionsFactory<TOptions> where TOptions : class, new() { private readonly IConfigureOptions<TOptions>[] _setups; private readonly IPostConfigureOptions<TOptions>[] _postConfigures; private readonly IValidateOptions<TOptions>[] _validations; public CultureOptionsFactory(IEnumerable<IConfigureOptions<TOptions>> setups, IEnumerable<IPostConfigureOptions<TOptions>> postConfigures, IEnumerable<IValidateOptions<TOptions>> validations) { _setups = setups as IConfigureOptions<TOptions>[] ?? setups.ToArray(); _postConfigures = postConfigures as IPostConfigureOptions<TOptions>[] ?? postConfigures.ToArray(); _validations = validations as IValidateOptions<TOptions>[] ?? validations.ToArray(); } public TOptions Create(string name) { var defaultOptions = this.CreateOptions(Options.DefaultName, default); if (string.IsNullOrEmpty(name)) { return defaultOptions; } var culture = CultureInfo.GetCultureInfo(name); var cultureStack = new Stack<CultureInfo>(); cultureStack.Push(culture); while (culture.Parent.Name.AsSpan().Length > 0) { culture = culture.Parent; cultureStack.Push(culture); } var options = defaultOptions; while (cultureStack.TryPop(out var next)) { options = this.CreateOptions(next.Name, options); } return options; } private TOptions CreateOptions(string name, TOptions? options) { if (options == null) { options = new TOptions(); } foreach (var setup in _setups) { if (setup is IConfigureNamedOptions<TOptions> namedSetup) { namedSetup.Configure(name, options); } else if (name == Options.DefaultName) { setup.Configure(options); } } foreach (var post in _postConfigures) { post.PostConfigure(name, options); } if (_validations != null) { var failures = new List<string>(); foreach (var validate in _validations) { var result = validate.Validate(name, options); if (result != null && result.Failed) { failures.AddRange(result.Failures); } } if (failures.Count > 0) { throw new OptionsValidationException(name, typeof(TOptions), failures); } } return options; } }
工程实现
----------------- | | JsonConfigurationSource -> | Configuration | -> IOptionsLocalizer<TOptions> | | -----------------
项目地址:
OptionsLocalization