使用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);
参考:
我有一个 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);
参考: