在 .Net 中默认通过 appsettings.json 的方式配置设置项。
在大多数应用场景中这是够用的,但是如果需要实现自己的配置中心, 则应该支持自定义的配置源。特别地,如果需要将配置加入 IOptions 中,则还需要实现自定义的 OptionsProvider
自定义配置源
Asp.Net 默认配置源如下:
默认情况下会按照配置的顺序
如果系统使用了自建的配置中心,则通过自定义配置源,加载配置中心的配置。假设自定的配置中心通过唯一的 Id 可以获取一条配置数据:/api/cms/id (cms means Config Manage System, 配置管理系统)
具体思路如下:
-
实现自定义的 ConfigurationProvider,可以根据 Id 从配置中心加载数据源为了提高性能,可以缓存所有调用
CmsConfigure时传递的 Id, 在 ConfigurationProvider 的 Setup 中一次加载数据。 -
提供扩展方法
services.CmsConfigure<Type>(id),支持让用户通过 Id 配置字段 -
实现自定义 Options, 支持自定义的字段解析逻辑
具体流程如下:
实现步骤
准备一些基础数据类型:
DataCenter 是示例从配置中心获取数据的类
// 示例代码,模拟实际获取配置的逻辑
internal class DataCenter
{
public Dictionary<string, object> BatchGet(string[] id)
{
// 这里写实际获取数据的逻辑,为方便调试, 可以配置任意类型
return Enumerable.Aggregate(id, new Dictionary<string, object>(), (acc, cur) =>
{
acc[cur] = new object();
return acc;
});
}
}
CmsConfigurationMetadata 为对配置项的元数据封装,包括id, 类型,以及在 Configuration 中的 Key 组建方式,这样可以保证对这些数据的操作内聚, 实际上也可以在各个地方随用随取, 不过代码可能会比较丑陋。
public class CmsConfigurationMetadata(Type type, string name, string id)
{
public Type Type { get; } = type;
public string Name { get; set; } = name;
public string Id { get; set; } = id;
// 只要类型和配置名一样,则认为是同一个配置
public override int GetHashCode() => HashCode.Combine(Type, Name);
public override bool Equals(object? obj) => obj is CmsConfigurationMetadata other && Type == other.Type && Name == other.Name;
// 自定义配置项的 Key,此Key对外部隐藏, 外部只需要根据 Id 便能获取对应配置
// 添加 CMS 前缀是为了添加数据源标识,防止key冲突,这个可自行组织Key
internal string CmsConfigurationKey() => $"CMS:{Name}:{Id}";
internal string CmsConfigurationKey(string property) => $"CMS:{Name}:{Id}:{property}";
internal static string CmsConfigurationKey(string name, string id) => $"CMS:{name}:{id}";
}
1. 实现自定义数据源
定义 CmsConfigurationProvider 和 CmsConfigurationSource
ConfigurationSource : 确当数据源
ConfigurationProvider : 确定如何从数据源中获取数据,并加载到 Data 字典中
Data 字典 ConfigurationProvider 基类的属性,且字典中的 key 不区分大小写
internal class CmsConfigurationProvider(DataCenter data) : ConfigurationProvider
{
private readonly DataCenter _data = data;
// 缓存配置项元数据,在Load中一次加载
private static readonly HashSet<CmsConfigurationMetadata> _optionsMetadata = [];
public override void Load()
{
var ids = _optionsMetadata.Select(m => m.Id).ToArray();
// 一次性获取所有配置信息项
var data = _data.BatchGet(ids);
foreach (var metadata in _optionsMetadata)
{
try
{
var dataItem = data[metadata.Id];
foreach (var property in metadata.Type.GetProperties())
{
Data[metadata.CmsConfigurationKey(property.Name)] = property.GetValue(dataItem)?.ToString();
}
}
catch (Exception ex)
{
throw new Exception($"CmsProvider load error: {ex}");
}
}
}
public static void AddOptionsMetadata(CmsConfigurationMetadata options)
{
// 与.net默认行为一致: 重复的配置项会被覆盖,只保留最后一次配置的内容
_optionsMetadata.Remove(options);
_optionsMetadata.Add(options);
}
}
// 配置数据源
internal class CmsConfigurationSource(IServiceCollection services) : IConfigurationSource
{
private readonly ServiceProvider _sp = services.BuildServiceProvider();
public IConfigurationProvider Build(IConfigurationBuilder builder)
=> new CmsConfigurationProvider(_sp.GetRequiredService<DataCenter>());
}
到这里实际上已经可以从配置中心加载配置了, 不过使用方式比较繁琐, 具体如下:
// 在 Program.cs 中配置, 如果是StartUp 则在 ConfigureService() 方法中
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(x => new DataCenter());
var id = "xxxxxxx"
// 配置 某一条id 的配置项
// 这里的 MyObject 可以是任意强类型
var metadata = new CmsConfigurationMetadata(typeof(MyObject), name, id);
CmsConfigurationProvider.AddOptionsMetadata(metadata);
// 加载 Provider, 此时会调用 CmsConfigurationProvider 的 Load 方法加载已经缓存过的配置项
builder.Configuration.Add(new CmsConfigurationSource(services));
// 获取配置 (如果是 StartUp 则在 Configure 方法中
var app = builder.Build();
app.Configuration.GetSection(CmsConfigurationMetadata.CmsConfigurationKey(name, id)).Get<MyObject>();
这样配置有两个坏处:
-
操作比较繁琐,每次配置Id都要手动初始化 CmsConfigurationMetadata 并加载到缓存中
-
对外部系统暴露了 CmsConfigurationMetadata 和 AddOptionsMetadata 方法,这是我们所不希望看到的
封装扩展方法
可以封装配置和获取配置的扩展方法,是的对自定义配置的使用更方便,且一定程度上保护了内部的字段和方法,这一步比较简单,具体如下:
public static class CmsConfigurationExtensions
{
public static IServiceCollection BuildCmsConfiguration(this IServiceCollection services, IConfigurationBuilder builder)
{
// 注册获取数据源的服务
services.AddSingleton(x => new DataCenter());
builder.Add(new CmsConfigurationSource(services));
return services;
}
#region 配置入口
public static IServiceCollection CmsConfigure<TOptions>(this IServiceCollection services, string id)
where TOptions : class
=> CmsConfigure<TOptions>(services, Options.DefaultName, id);
public static IServiceCollection CmsConfigure<TOptions>(this IServiceCollection services, string name, string id)
where TOptions : class
{
var metadata = new CmsConfigurationMetadata(typeof(TOptions), name, id);
CmsConfigurationProvider.AddOptionsMetadata(metadata);
// 如果不需要自定义 CmsContentConfigureOptions 则直接 return services 即可
// 自定义 Optioins 下面会讲到
return services.AddSingleton<IConfigureOptions<TOptions>>(sp => new CmsContentConfigureOptions<TOptions>(metadata, sp.GetRequiredService<IConfiguration>()));
}
#endregion
#region 获取配置值
public static T? GetCmsConfig<T>(this IConfiguration configuration, string id) where T : class
{
return configuration.GetNamedCmsConfig<T>(Options.DefaultName, id);
}
public static T? GetNamedCmsConfig<T>(this IConfiguration configuration, string name, string id) where T : class
{
return configuration.GetSection(CmsConfigurationMetadata.CmsConfigurationKey(name, id)).Get<T>();
}
#endregion
}
这样上面 Program 中加载配置和获取配置的方法就变得很简单
// 在 Program.cs 中配置, 如果是StartUp 则在 ConfigureService() 方法中
var builder = WebApplication.CreateBuilder(args);
var id = "xxxxxxx"
// 配置 某一条id 的配置项
builder.Services
.CmsConfigure<MyObject>(id)
.BuildCmsConfiguration(builder.Configuration);
// 获取配置 (如果是 StartUp 则在 Configure 方法中
var app = builder.Build();
app.Configuration.GetCmsConfig<MyObject>(id);
此时 CmsConfigurationMetadata 就可以定义为 Internal 而不需要暴露出来
internal class CmsConfigurationMetadata(...){...}
IOptions
如果需要从 IOptions<> 中获取配置信息, 这在依赖注入中很常见, 则需要定义 IConfigureOptions<> 的实现, 因为上面我么的配置支持命名配置, 所以在此处可以直接实现命名选项 IConfigureNamedOptions
internal class CmsConfigureOptions<TOptions> : IConfigureNamedOptions<TOptions> where TOptions : class
{
private readonly IConfiguration _cofiguration;
private readonly CmsConfigurationMetadata _metaData;
internal CmsConfigureOptions(CmsConfigurationMetadata metaData, IConfiguration cofiguration)
{
_metaData = metaData;
_cofiguration = cofiguration;
}
public void Configure(string? name, TOptions options)
{
ArgumentNullException.ThrowIfNull(options);
// 默认行为: 如果按名字配置, Options.DefaultName 取不到值
if (name == null || name == _metaData.Name)
{
var configuredOptions = GetOptions();
// 使用反射给 options 的属性赋值
configuredOptions?.GetType().GetProperties().ToList().ForEach(p =>
{
var value = p.GetValue(configuredOptions);
p.SetValue(options, value);
});
}
}
public void Configure(TOptions options) => Configure(Options.DefaultName, options);
// 如果有其他获取配置的方式,可以在这里扩展, 例如直接从字符串解析等
protected TOptions? GetOptions() => _cofiguration.GetSection(_metaData.CmsConfigurationKey()).Get<TOptions>();
}
可以直接在 API 或其他注入的服务中使用:
app.MapGet("fromOptions", (IOptions<MyObject> options) => options.Value);