IConfigurationRoot 和类型信息

IConfigurationRoot and type information

据此,您可以放入JSON的有限类型信息似乎被丢弃了。

You seem to be under the impression that IConfiguration objects are storing ints, bools, etc. (for example) corresponding to the JSON Element type. This is incorrect. All data within an IConfiguration is stored in stringified form. The base Configuration Provider classes all expect an IDictionary<string, string> filled with data. Even the JSON Configuration Providers perform an explicit ToString on the values.

当我用这个扩展方法解决那个问题中提出的问题时,我注意到了这一点。

using System.Collections.Generic;
using System.Dynamic;
using Microsoft.Extensions.Configuration;

public static class ExtendConfig
{
  public static dynamic AsDynamic(this IConfigurationRoot cr)
  {
    var result = new ExpandoObject();
    var resultAsDict = result as IDictionary<string, object>;
    foreach (var item in cr.AsEnumerable())
    {
      resultAsDict.Add(item.Key, item.Value);
    }
    return result;
  }
}

此方法重建图形,但现在一切都是字符串。

我可以编写自己的解析器并将其应用于原始 JSON 字符串,但这有点可怕。有什么方法可以获得此元数据,以便提高合并配置的保真度?我正在传递它供 JS 使用,它确实注意到了差异。

合并是我使用配置扩展构建器的原因。

由于 IConfiguration 不提供有关类型的信息,但 JsonConfigurationProvider 使用的 System.Text.Json 提供,工作解决方案(或解决方法)将使用 System.Text.Json反序列化器直接读取配置文件并将类型与配置键匹配。

但我们有一些小问题需要先解决。比如——配置文件在哪里?我们不想在代码中复制该信息,我们必须从 IConfiguration 实例中提取它。

然后 - 将具体的现有配置键与 JSON 文档树节点匹配。这将需要 DFS 或 BFS 树遍历算法。我会选择 DFS(深度优先搜索)。简而言之 - 如果您有可扩展节点,则将它们以相反的顺序放入堆栈中。然后你有一个 while 循环从堆栈中获取一个节点,如果它有子节点,你将它们放在同一个堆栈中,如果没有 - 你只是产生节点。就这么简单,和 BFS 很相似,但没关系。

还有一件事:Newtonsoft.Json - 一个流行的 Nuget 程序包,甚至微软也曾使用过。 JSON 序列化程序比 System.Text.Json 慢一点,但它更高级,允许用户逐个节点构建 JSON 文档树。

有了这个强大的工具,创建可写的 JSON IConfiguration 变得相对容易,尤其是使用像下面我这样的助手。

查看SaveChanges()方法。它遍历 IConfiguration 节点,通过路径匹配适当的 JObject 节点,并将更改从 IConfiguration 实例复制到 JObject 实例。然后你可以只写 JSON 文件。

有一个丑陋的 hack 用于获取文件。我得到了包含 IConfigurationRoot 实例的私有字段,但如果您已经拥有配置根目录,则可以跳过该字段。有了根你可以从中得到JsonConfigurationProvider,然后就是Source.Path 属性.

这是代码。它是 Woof.ToolkitWoof.Config Nuget 包的一部分,提供可写的 JSON 配置、一些辅助方法,以及使用 JSON 配置,使用一些辅助方法使用存储在 AKV 上的密钥加密和解密敏感数据。

这是 ConfigurationExtensions class 的第一个版本,因此它在性能方面可能不是最理想的,但它有效并说明了如何匹配 IConfiguration 实例节点使用 JObject 个节点获取配置属性的类型。

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;

using Newtonsoft.Json.Linq;

namespace Woof.Config;

/// <summary>
/// Extensions for <see cref="IConfiguration"/> making JSON type <see cref="IConfiguration"/> writeable.
/// </summary>
public static class ConfigurationExtensions {


    /// <summary>
    /// Gets the configuration root element.
    /// </summary>
    /// <param name="configuration">Any <see cref="IConfiguration"/> part.</param>
    /// <returns>Root element.</returns>
    public static IConfigurationRoot? GetRoot(this IConfiguration configuration) {
        if (configuration is IConfigurationRoot root) return root;
        var rootField = configuration.GetType().GetField("_root", BindingFlags.Instance | BindingFlags.NonPublic);
        return rootField?.GetValue(configuration) as IConfigurationRoot;
    }

    /// <summary>
    /// Gets the first <see cref="JsonConfigurationProvider"/> if exists, null otherwise.
    /// </summary>
    /// <param name="root">Configuration root element.</param>
    /// <returns><see cref="JsonConfigurationProvider"/> or null.</returns>
    public static JsonConfigurationProvider? GetJsonConfigurationProvider(this IConfigurationRoot root)
        => root.Providers.OfType<JsonConfigurationProvider>().FirstOrDefault();

    /// <summary>
    /// Gets the first <see cref="JsonConfigurationProvider"/> if exists, null otherwise.
    /// </summary>
    /// <param name="config">Any <see cref="IConfiguration"/> part.</param>
    /// <returns><see cref="JsonConfigurationProvider"/> or null.</returns>
    public static JsonConfigurationProvider? GetJsonConfigurationProvider(this IConfiguration config)
        => config.GetRoot()?.GetJsonConfigurationProvider();

    /// <summary>
    /// Saves changes made to <see cref="IConfiguration"/> to the JSON file if exists.
    /// </summary>
    /// <param name="config">Any <see cref="IConfiguration"/> part.</param>
    /// <exception cref="InvalidOperationException">Configuration does not have <see cref="JsonConfigurationProvider"/>.</exception>
    public static void SaveChanges(this IConfiguration config) {
        var provider = config.GetJsonConfigurationProvider();
        if (provider is null) throw new InvalidOperationException("Can't get JsonConfigurationProvider");
        var sourceJson = File.ReadAllText(provider.Source.Path);
        var target = JObject.Parse(sourceJson);
        var stack = new Stack<IConfigurationSection>();
        foreach (IConfigurationSection section in config.GetChildren().Reverse()) stack.Push(section);
        while (stack.TryPop(out var node)) {
            var children = node.GetChildren();
            if (children.Any()) foreach (var child in children.Reverse()) stack.Push(child);
            else {
                var jPath = GetJPath(node.Path);
                var element = target.SelectToken(jPath);
                var valueString =
                    element!.Type == JTokenType.Null
                        ? "null" :
                        element!.Type == JTokenType.String ? $"\"{node.Value}\"" : node.Value;
                element!.Replace(JToken.Parse(valueString));
            }
        }
        File.WriteAllText(provider.Source.Path, target.ToString());
    }

    /// <summary>
    /// Sets <paramref name="configuration"/>'s <paramref name="key"/> with specified <paramref name="value"/>.
    /// </summary>
    /// <param name="configuration">The configuration.</param>
    /// <param name="key">The key of the configuration section.</param>
    /// <param name="value">Value to set.</param>
    /// <exception cref="InvalidOperationException">Not supported type as value.</exception>
    public static void SetValue(this IConfiguration configuration, string key, object? value) {
        var c = CultureInfo.InvariantCulture;
        var valueString = value switch {
            null => null,
            string v => v,
            Uri v => v.ToString(),
            byte[] v => Convert.ToBase64String(v),
            bool v => v.ToString(c),
            int v => v.ToString(c),
            decimal v => v.ToString(c),
            double v => v.ToString(c),
            uint v => v.ToString(c),
            long v => v.ToString(c),
            ulong v => v.ToString(c),
            short v => v.ToString(c),
            ushort v => v.ToString(c),
            byte v => v.ToString(c),
            sbyte v => v.ToString(c),
            float v => v.ToString(c),
            _ => throw new InvalidOperationException($"Cannot set value of type {value.GetType()}")
        };
        configuration[key] = valueString;
    }

    /// <summary>
    /// Gets the path for JObject.SelectToken method.
    /// </summary>
    /// <param name="path"><see cref="IConfiguration"/> path.</param>
    /// <returns><see cref="JObject"/> path.</returns>
    private static string GetJPath(string path) => RxIConfigurationIndex.Replace(path, "[]").Replace(':', '.');

    /// <summary>
    /// Matches the <see cref="IConfiguration"/> indices.
    /// </summary>
    private static readonly Regex RxIConfigurationIndex = new(@":(\d+)", RegexOptions.Compiled);

}

为什么JObject? JSON 文件可以只表示一个对象吗?否 - 它可以表示任何值,包括 null。但是 JSON 配置必须是一个对象。这就是为什么我使用 JObject 作为我的辅助配置根。