前言
选项模式 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
json
{
"OptionsLocalization": {
"AIOptions": {
"zh-CN": {
"ModelId": "gemini2.5",
"Prompt": "你好世界"
}
}
}
}
期待简化后的 AIOptions/zh-CN.json
json
{
"ModelId": "gemini2.5",
"Prompt": "你好世界"
}
3 语言区域别名化的 Options
c#
// 选项绑定到别名化的配置
// 如果是默认语言区域,则注册成别名为Options.DefaultName
services.Configure<AIOptions>("zh-CN", configuration.GetSection("OptionsLocalization:AIOptions:zh-CN"));
c#
// 使用别名获取选项
IOptionsMonitor<AIOptions>().Get("zh-CN");
4 支持父语言区域回退
假设注册"en"默认语言和zh语言
c#
services.Configure<AIOptions>("zh", zhSection);
services.Configure<AIOptions>(Options.DefaultName, enSection);
现在前端的语言区域为"zh-CN",IOptionsMonitor<AIOptions>().Get("zh-CN")
会存在以下问题:
zh-CN
不存在,要回退到zh-Hans
zh-Hans
不存在,要回退到zh
zh
下的ModelId
没有配置项,要回退使用默认的en
下的ModelId
项
我们需要实现自定义的IOptionsFactory<TOptions>
,把指定的语言区域别名的AIOptions
构建正确。
c#
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