所以假设 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);
}
我们可以在这里看到,我们正在做的是一个浅层更新。如果对象包含另一个复杂对象作为其属性,则该对象将作为一个整体被复制和覆盖,而不是更新。如果您需要深度更新,则需要更改此方法以提取属性的当前值,然后PopulateObject
在属性的类型是引用类型时递归调用(这也需要Type
在 中作为参数接受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
,您必须更深入地挖掘以获得 aJsonElement
的值。corefx 存储库中有一个活跃的问题,允许直接反序列化JsonElement
. 目前最接近的解决方案是使用GetRawText()
. 我继续编辑上面的代码以工作,留下旧版本。