动手造轮子 -- 配置检查器
Intro
在我们开发过程中往往会用到配置,那么能不能比较方便地获取当前某一个配置的值呢?一个配置有多个配置源的时候哪一个是真正生效的那个呢?某些配置是可能会动态更新的,比如从配置中心获取的值,配置中心更新之后,当前应用配置的值是什么呢,有没有被成功更新呢?
带着这些问题,就想如果可以实现一个实时查看当前配置的功能就好了,于是就有了这个配置检查器的轮子
Sample
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapConfigInspector();
await app.RunAsync();
从 config-inspector 的界面我们可以看到当前所有的 configuration 中的配置的 key 以及 value 以及当前是否生效
比如 FeatureFlags:ConfigInspector
这个配置,在 appsettings.json 和 appsettings.Development.json 中都有,
当前的 environment 是 development 所以 Development.json 中的配置会 override appsettings.json 中的配置
appsettings.development.json
中的配置的 active
是 true
,而 appsettings.json
中的 active
则是 false
Implement
dotnet 中的 root IConfiguration
是一个 IConfigurationRoot
对象,包含了多个 IConfigurationProvider
,不同的 IConfigurationProvider
代表了不同的配置源/配置提供者
public interface IConfigurationRoot : IConfiguration
{
void Reload();
IEnumerable<IConfigurationProvider> Providers { get; }
}
我们可以从 IConfiguration
中拿到所有的配置信息,再从 IConfigurationProvider
中拿到每个配置源中的配置值,这样我们基本就能拿到所有配置源中的配置了,这里有一个特殊的配置源 ChainedConfigurationProvider
可能会有多个 IConfigurationProvider
组成,从 .NET 7 开始 IConfiguration
也 public
了出来
public class ChainedConfigurationProvider : IConfigurationProvider
{
public IConfiguration Configuration { get; }
}
这里我们就可以通过模式匹配拿到 ChainedConfigurationProvider
中的具体的 IConfiguration
以及更近一步地获取内部的 IConfigurationProvider
public sealed class ConfigModel
{
public string Provider { get; set; } = default!;
public ConfigItemModel[] Items { get; set; } = [];
}
public sealed class ConfigItemModel
{
public string Key { get; set; } = default!;
public string? Value { get; set; }
public bool Active { get; set; }
}
private static ConfigModel[] GetConfig(IConfigurationRoot configurationRoot, )
{
var allKeys = configurationRoot.AsEnumerable()
.ToDictionary(x => x.Key, _ => false);
var providers = GetConfigProviders(configurationRoot);
var config = new ConfigModel[providers.Count];
for (var i = providers.Count - 1; i >= 0; i--)
{
var provider = providers[i];
config[i] = new ConfigModel
{
Provider = provider.ToString() ?? provider.GetType().Name,
Items = GetConfig(provider, allKeys).ToArray()
};
}
return config;
}
private static List<IConfigurationProvider> GetConfigProviders(IConfigurationRoot configurationRoot)
{
var providers = new List<IConfigurationProvider>();
foreach (var provider in configurationRoot.Providers)
{
if (provider is not ChainedConfigurationProvider chainedConfigurationProvider)
{
providers.Add(provider);
continue;
}
if (chainedConfigurationProvider.Configuration is not IConfigurationRoot chainsConfigurationRoot)
{
continue;
}
providers.AddRange(GetConfigProviders(chainsConfigurationRoot));
}
return providers;
}
这里用了一个 bool 值来记录是否已经找到了 config,第一次找到之后应该将它设置为 true
,当前的 IConfigurationProvider
的配置是实际生效的,其他的 IConfigurationProvider
这个 config 的配置是不生效的
这里从 provider 读的顺序要注意应该要倒序读,因为实际 IConfiguration
获取值的时候也是倒序读的,因此后面添加的 Configuration
才会有更高的优先级
GetConfigProviders
方法里针对 ChainedConfigurationProvider
写了一个递归,用于获取它内部的 IConfigurationProvider
config 已经可以获取到了,我们再来写个中间件来实现查看我们的配置信息吧
为了增加中间件的可扩展性,我们引入一个 options model
public sealed class ConfigInspectorOptions
{
public Func<HttpContext, ConfigModel[], Task>? ConfigRenderer { get; set; }
}
options 中定义了一个 ConfigRenderer
通过这个委托我们可以自定义 config 渲染,默认我们可以直接输出一个 JSON
internal sealed class ConfigInspectorMiddleware(RequestDelegate next)
{
public Task InvokeAsync(HttpContext httpContext, IOptions<ConfigInspectorOptions> inspectorOptions)
{
var configuration = httpContext.RequestServices.GetRequiredService<IConfiguration>();
if (configuration is not IConfigurationRoot configurationRoot)
{
throw new NotSupportedException(
"Support ConfigurationRoot configuration only, please use the default configuration or implement IConfigurationRoot");
}
var inspectorOptionsValue = inspectorOptions.Value;
var configs = GetConfig(configurationRoot, inspectorOptionsValue);
if (inspectorOptionsValue.ConfigRenderer is null)
return httpContext.Response.WriteAsJsonAsync(configs);
return inspectorOptionsValue.ConfigRenderer.Invoke(httpContext, configs);
}
}
internal static IApplicationBuilder UseConfigInspector(this IApplicationBuilder app,
Action<ConfigInspectorOptions>? optionsConfigure = null)
{
ArgumentNullException.ThrowIfNull(app);
if (optionsConfigure is not null)
{
var options = app.ApplicationServices.GetRequiredService<IOptions<ConfigInspectorOptions>>();
optionsConfigure(options.Value);
}
return app.UseMiddleware<ConfigInspectorMiddleware>();
}
为了更好地使用 asp.net core 的 endpoint routing,我们通过不直接通过使用中间件的方式使用,通过下面的代码注册一个端点路由
public static IEndpointConventionBuilder MapConfigInspector(this IEndpointRouteBuilder endpointRouteBuilder, string path = "/config-inspector", Action<ConfigInspectorOptions>? optionsConfigure = null)
{
ArgumentNullException.ThrowIfNull(endpointRouteBuilder);
var app = endpointRouteBuilder.CreateApplicationBuilder();
var pipeline = app.UseConfigInspector(optionsConfigure).Build();
return endpointRouteBuilder.MapGet(path, pipeline);
}
这里我们就完成了我们前面示例的效果了,在实际测试中会发现有一些没有任何配置的 IConfigurationProvider
,大部分时候可能并不关心这种配置源,所以后面增加了一个 IncludeEmptyProviders
默认值是 false
,有需要可以 enable
public sealed class ConfigInspectorOptions
{
public bool IncludeEmptyProviders { get; set; }
public Func<HttpContext, ConfigModel[], Task>? ConfigRenderer { get; set; }
}
然后中间件的逻辑也要稍微改造一下
if (options.IncludeEmptyProviders)
{
return config;
}
return config.Where(x => x.Items is { Length: > 0 }).ToArray();
因为默认的 dotnet 会读取所有的环境变量,config 可能会比较多,特别是在 k8s 环境中会有很多可能并不关心的环境变量出现,因此想要增加一个小功能,指定 config key 只返回这个 key 相关的配置
在前面路由里添加一个可选参数:
endpointRouteBuilder.MapGet($"{path}/{
{configKey?}}", pipeline);
然后在中间件处理逻辑增加一个过滤的逻辑
var configKey = string.Empty;
if (httpContext.Request.RouteValues.TryGetValue("configKey", out var configKeyObj) &&
configKeyObj is string { Length: > 0 } configKeyName)
{
configKey = configKeyName;
}
var configs = GetConfig(configurationRoot, inspectorOptionsValue, configKey);
private static ConfigModel[] GetConfig(IConfigurationRoot configurationRoot, ConfigInspectorOptions options,
string configKey)
{
var allKeys = configurationRoot.AsEnumerable()
.ToDictionary(x => x.Key, _ => false);
var hasConfigKeyFilter = !string.IsNullOrEmpty(configKey);
if (hasConfigKeyFilter)
{
if (allKeys.TryGetValue(configKey, out _))
{
allKeys = new()
{
{ configKey, false }
};
}
else
{
return [];
}
}
// ...
}
来测试一下:
可以看到这里只返回了有这个配置的 IConfigurationProvider
相关的信息
有时候查看 json 可能并不方便,我们也可以通过自定义渲染的方式来扩展自己的渲染方式,示例如下:
app.MapConfigInspector(optionsConfigure: options =>
{
options.ConfigRenderer = async (context, configs) =>
{
var htmlStart = """
<html>
<head>
<title>Config Inspector</title>
</head>
<body>
<table style="font-size:1.2em;line-height:1.6em">
<thead>
<tr>
<th>Provider</th>
<th>Key</th>
<th>Value</th>
<th>Active</th>
</tr>
</thead>
<tbody>
""";
var htmlEnd = "</tbody></table></body></html>";
var tbody = new StringBuilder();
foreach (var config in configs)
{
tbody.Append($"<tr><td>{config.Provider}</td>");
foreach (var item in config.Items)
{
tbody.Append($$"""<td>{
{item.Key}}</td><td>{
{item.Value}}</td><td><input type="checkbox" {
{(item.Active ? "checked" : "")}} /></td>""");
}
tbody.AppendLine("</tr>");
}
var responseText = $"{htmlStart}{tbody}{htmlEnd}";
await context.Response.WriteAsync(responseText);
};
})
这里返回了一个简单的 html,返回了一个 table 来展示配置,效果如下:
需要的话也可以自己定制一个模板,根据模板进行渲染
More
之所以使用 endpoint routing 主要是扩展一些别的功能比较方便,比如增加授权验证
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication(ApiKeyAuthenticationDefaults.AuthenticationSchema)
.AddApiKey(options =>
{
options.ApiKey = "123456";
options.ApiKeyName = "X-ApiKey";
})
;
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapConfigInspector()
.RequireAuthorization(x => x
.AddAuthenticationSchemes("ApiKey")
.RequireAuthenticatedUser()
)
;
await app.RunAsync();
增加了 auth 之后,直接访问就会返回 401
使用 dotnet-httpie 发送 http request 也是一样的
当提供了 X-ApiKey
之后就可以正常返回了
完整代码可以从 github 获取,处理逻辑的开始是实现了自定义中间件,后来改造成了 endpoint routing,也可以考虑将逻辑抽离出来直接放在 endpoint routing 注册的地方
References
-
https://github.com/WeihanLi/WeihanLi.Web.Extensions/blob/dev/samples/WeihanLi.Web.Extensions.Samples/Program.cs
-
https://github.com/WeihanLi/WeihanLi.Web.Extensions/blob/dev/src/WeihanLi.Web.Extensions/Middleware/ConfigInspectorMiddleware.cs
-
https://github.com/WeihanLi/WeihanLi.Web.Extensions/blob/dev/src/WeihanLi.Web.Extensions/Extensions/EndpointExtensions.cs
-
https://github.com/dotnet/runtime/blob/9a4329d65c4d272e26b2c693341ef4b26a5bc8c8/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationRoot.cs#L114
文章评论