使用Newtonsoft解析.net 5中的配置文件

use Newtonsoft to parse Configuration files in .net 5

我有一个 json 配置文件,我想在其中存储一个可以包含多个未知字段的 json 对象。

我可以使用 Newtonsoft 通过从这个基础派生来反序列化这样一个对象 class:

public class ComplexJsonObject
{
    [JsonExtensionData]
    private IDictionary<string, JToken> _additionalData_newtonsoft;
}

不幸的是,配置文件 appsettings.development.json 似乎只是说我有一个 空对象。即使配置了一些东西。

我认为这是因为系统使用了 System.Text.Json。 所以我也试过了:

public class ComplexJsonObject
{
    [JsonExtensionData]
    private IDictionary<string, JToken> _additionalData_newtonsoft;

    [System.Text.Json.Serialization.JsonExtensionData]
    [Newtonsoft.Json.JsonIgnore]
    public IDictionary<string, JsonElement> _additionalData_dotnet { get; set; }
}

这也不行。

所以问题是:如何告诉系统使用 Newtonsoft 反序列化此配置文件?

-- 编辑--

根据要求,我要存储的配置示例。 配置键将是“configuration:when”,我期望的对象必须有一个运算符,但所有其他字段都是动态的。

{
    "extraction": {
        "when": {
            "operator": "and",
            "rules": [
            {
                "operator": "or",
                "rules": [ 
                { "operator": "field.contains", "value": "waiting" },
                { "operator": "field.contains", "value": "approved" },
                { "operator": "field.contains", "value": "rejected" }
                ]
            },
            { "operator": "not", "rule": { "operator": "field.contains", "value": "imported" } },
            { "operator": "not", "rule": { "operator": "field.contains", "value": "import-failed" } }
            ]
        }
    }
}   

我觉得Métoule说得对,这确实不行。由于默认情况下配置会混合来自其他文件的值。

您想要的是不可能的,因为 .NET 配置系统不是这样工作的。该配置不会直接将您的 JSON 解析为您的数据结构;相反,它会创建一个中间表示(这是一个简单的 IDictionary<string,string>),然后将其绑定到您的数据结构。

之所以不是直接映射是因为配置数据可以来自多个来源。例如,可以使用通过 Azure 门户 UI 指定的值覆盖 JSON 配置。或者可能根本没有 JSON 文件。

也就是说,滥用配置系统是可能的,就像我在以下问题中解释的那样:

如果您只需要使用 newtonsoft 转换特定部分,您可以使用此扩展程序:

public static class ConfigurationBinder
{
    public static void BindJsonNet<T>(this IConfiguration config, T instance) where T : class
    {
        string objectPath = config.AsEnumerable().Select(x => x.Key).FirstOrDefault();

        var obj = BindToExpandoObject(config, objectPath);
        if (obj == null)
            return;

        var jsonText = JsonConvert.SerializeObject(obj);
        var jObj = JObject.Parse(jsonText);
        if (jObj == null)
            return;

        var jToken = jObj.SelectToken($"*.{GetLastObjectName(objectPath)}");
        if (jToken == null)
            return;

        jToken.Populate<T>(instance);
    }

    private static ExpandoObject BindToExpandoObject(IConfiguration config, string objectPath)
    {
        var result = new ExpandoObject();
        string lastObjectPath = GetLastObjectPath(objectPath);

        // retrieve all keys from your settings
        var configs = config.AsEnumerable();
        configs = configs
            .Select(x => new KeyValuePair<string, string>(x.Key.Replace(lastObjectPath, ""), x.Value))
            .ToArray();

        foreach (var kvp in configs)
        {
            var parent = result as IDictionary<string, object>;
            var path = kvp.Key.Split(':');

            // create or retrieve the hierarchy (keep last path item for later)
            var i = 0;
            for (i = 0; i < path.Length - 1; i++)
            {
                if (!parent.ContainsKey(path[i]))
                {
                    parent.Add(path[i], new ExpandoObject());
                }

                parent = parent[path[i]] as IDictionary<string, object>;
            }

            if (kvp.Value == null)
                continue;

            // add the value to the parent
            // note: in case of an array, key will be an integer and will be dealt with later
            var key = path[i];
            parent.Add(key, kvp.Value);
        }

        // at this stage, all arrays are seen as dictionaries with integer keys
        ReplaceWithArray(null, null, result);

        return result;
    }

    private static string GetLastObjectPath(string objectPath)
    {
        string lastObjectPath = objectPath;
        int indexLastObj;
        if ((indexLastObj = objectPath.LastIndexOf(":")) != -1)
            lastObjectPath = objectPath.Remove(indexLastObj);
        return lastObjectPath;
    }

    private static string GetLastObjectName(string objectPath)
    {
        string lastObjectPath = objectPath;
        int indexLastObj;
        if ((indexLastObj = objectPath.LastIndexOf(":")) != -1)
            lastObjectPath = objectPath.Substring(indexLastObj + 1);
        return lastObjectPath;
    }

    private static void ReplaceWithArray(ExpandoObject parent, string key, ExpandoObject input)
    {
        if (input == null)
            return;

        var dict = input as IDictionary<string, object>;
        var keys = dict.Keys.ToArray();

        // it's an array if all keys are integers
        if (keys.All(k => int.TryParse(k, out var dummy)))
        {
            var array = new object[keys.Length];
            foreach (var kvp in dict)
            {
                array[int.Parse(kvp.Key)] = kvp.Value;
            }

            var parentDict = parent as IDictionary<string, object>;
            parentDict.Remove(key);
            parentDict.Add(key, array);
        }
        else
        {
            foreach (var childKey in dict.Keys.ToList())
            {
                ReplaceWithArray(input, childKey, dict[childKey] as ExpandoObject);
            }
        }
    }

    public static void Populate<T>(this JToken value, T target) where T : class
    {
        using (var sr = value.CreateReader())
        {
            JsonSerializer.CreateDefault().Populate(sr, target); // Uses the system default JsonSerializerSettings
        }
    }
}

用法:

var obj = new SampleObject();
Configuration.GetSection("test:arr:3:sampleObj").BindJsonNet(obj);
services.AddSingleton(obj);

参考: