所以假设 Core 3 不支持这个开箱即用,让我们尝试解决这个问题。那么,我们的问题是什么?
我们想要一种方法,用 json 字符串中的属性覆盖现有对象的某些属性。所以我们的方法会有一个签名:
void PopulateObject<T>(T target, string jsonSource) where T : class
我们真的不想要任何自定义解析,因为它很麻烦,所以我们将尝试显而易见的方法 - 反序列化 jsonSource 并将结果属性复制到我们的对象中。然而,我们不能就这样走了
T updateObject = JsonSerializer.Parse<T>(jsonSource);
CopyUpdatedProperties(target, updateObject);
那是因为对于一个类型
class Example
{
int Id { get; set; }
int Value { get; set; }
}
和一个 JSON
{
"Id": 42
}
我们会收到updateObject.Value == 0。现在我们不知道0 是新的更新值还是只是没有更新,所以我们需要确切地知道jsonSource 包含哪些属性。
幸运的是,System.Text.Json API 允许我们检查解析后的 JSON 的结构。
using var json = JsonDocument.Parse(jsonSource).RootElement;
我们现在可以枚举所有属性并复制它们。
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
我们将使用反射复制该值:
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
我们可以在这里看到我们正在做的是一个浅的更新。如果对象包含另一个复杂对象作为其属性,则该对象将作为一个整体被复制和覆盖,而不是更新。如果您需要 deep 更新,则需要更改此方法以提取属性的当前值,然后如果属性的类型是引用类型,则递归调用 PopulateObject(这也需要接受 @ 987654335@作为PopulateObject中的参数)。
将它们结合在一起,我们得到:
void PopulateObject<T>(T target, string jsonSource) where T : class
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property);
}
}
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class
{
var propertyInfo = typeof(T).GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
v̶a̶r̶ ̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
var parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
propertyInfo.SetValue(target, parsedValue);
}
这有多强大?好吧,它肯定不会对 JSON 数组做任何明智的事情,但我不确定您如何期望 PopulateObject 方法开始在数组上工作。我不知道它与Json.Net 版本的性能相比如何,您必须自己测试。根据设计,它还会默默地忽略不在目标类型中的属性。我认为这是最明智的方法,但您可能会不这么认为,在这种情况下,属性 null-check 必须替换为异常抛出。
编辑:
我继续实施了一个深拷贝:
void PopulateObject<T>(T target, string jsonSource) where T : class =>
PopulateObject(target, jsonSource, typeof(T));
void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
OverwriteProperty(target, updatedProperty, typeof(T));
void PopulateObject(object target, string jsonSource, Type type)
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
foreach (var property in json.EnumerateObject())
{
OverwriteProperty(target, property, type);
}
}
void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶ ̶=̶ ̶J̶s̶o̶n̶S̶e̶r̶i̶a̶l̶i̶z̶e̶r̶.̶P̶a̶r̶s̶e̶(̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
P̶o̶p̶u̶l̶a̶t̶e̶O̶b̶j̶e̶c̶t̶(̶p̶a̶r̶s̶e̶d̶V̶a̶l̶u̶e̶,̶ ̶u̶p̶d̶a̶t̶e̶d̶P̶r̶o̶p̶e̶r̶t̶y̶.̶V̶a̶l̶u̶e̶,̶ ̶p̶r̶o̶p̶e̶r̶t̶y̶T̶y̶p̶e̶)̶;̶
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
要使其更加健壮,您要么必须有一个单独的 PopulateObjectDeep 方法,要么传递 PopulateObjectOptions 或带有深/浅标志的类似方法。
编辑 2:
深拷贝的意义在于,如果我们有一个对象
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 32
},
"Value": 128
}
并用
填充它
{
"Child":
{
"Value": 64
}
}
我们会得到
{
"Id": 42,
"Child":
{
"Id": 43,
"Value": 64
},
"Value": 128
}
如果是浅拷贝,我们会在被拷贝的孩子中得到Id = 0。
编辑 3:
正如@ldam 所指出的,这在稳定的 .NET Core 3.0 中不再适用,因为 API 已更改。 Parse 方法现在是 Deserialize,您必须深入挖掘才能获得 JsonElement 的值。 an active issue in the corefx repo 允许直接反序列化 JsonElement。目前最接近的解决方案是使用GetRawText()。我继续编辑上面的代码以使其正常工作,留下旧版本。