预赛
我将大量使用我对链接问题的回答中的现有代码:.Net Core 3.0 JsonSerializer populate existing object。
正如我所提到的,浅拷贝的代码可以工作并产生结果 2。所以我们只需要修复深拷贝的代码并让它产生结果 1。
在我的机器上,代码在isPopulateObject
时崩溃,因为它既不是值类型也不是 JSON 中的对象所表示的东西。我在原始答案中修复了这个问题,如果必须是:propertyType
typeof(string)
string
if (elementType.IsValueType || elementType == typeof(string))
落实新要求
好的,所以第一个问题是识别某个东西是否是一个集合。目前我们查看我们想要覆盖的属性的类型来做出决定,所以现在我们将做同样的事情。逻辑如下:
private static bool IsCollection(Type type) =>
type.GetInterfaces().Any(x => x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(ICollection<>));
所以我们认为集合的唯一东西是ICollection<T>
为 some实现的东西T
。我们将通过实现一个新PopulateCollection
方法来完全独立地处理集合。我们还需要一种方法来构造一个新集合——也许初始对象中的列表是null
,所以我们需要在填充它之前创建一个新集合。为此,我们将寻找它的无参数构造函数:
private static object Instantiate(Type type)
{
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Array.Empty<Type>());
if (ctor is null)
{
throw new InvalidOperationException($"Type {type.Name} has no parameterless constructor.");
}
return ctor.Invoke(Array.Empty<object?>());
}
我们允许它存在private
,因为为什么不呢。
现在我们对OverwriteProperty
:
private static void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
if (updatedProperty.Value.ValueKind == JsonValueKind.Null)
{
propertyInfo.SetValue(target, null);
return;
}
var propertyType = propertyInfo.PropertyType;
object? parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else if (IsCollection(propertyType))
{
var elementType = propertyType.GenericTypeArguments[0];
parsedValue = propertyInfo.GetValue(target);
parsedValue ??= Instantiate(propertyType);
PopulateCollection(parsedValue, updatedProperty.Value.GetRawText(), elementType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
parsedValue ??= Instantiate(propertyType);
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
最大的变化是if
语句的第二个分支。我们找出集合中元素的类型并从对象中提取现有集合。如果它为空,我们创建一个新的空的。然后我们调用新方法来填充它。
该PopulateCollection
方法将与OverwriteProperty
.
private static void PopulateCollection(object target, string jsonSource, Type elementType)
首先我们得到Add
集合的方法:
var addMethod = target.GetType().GetMethod("Add", new[] { elementType });
这里我们期望一个实际的 JSON 数组,所以是时候枚举它了。对于数组中的每个元素,我们需要做与 in 相同的事情OverwriteProperty
,这取决于我们是否有一个值、数组或对象,我们有不同的流程。
foreach (var property in json.EnumerateArray())
{
object? element;
if (elementType.IsValueType || elementType == typeof(string))
{
element = JsonSerializer.Deserialize(jsonSource, elementType);
}
else if (IsCollection(elementType))
{
var nestedElementType = elementType.GenericTypeArguments[0];
element = Instantiate(elementType);
PopulateCollection(element, property.GetRawText(), nestedElementType);
}
else
{
element = Instantiate(elementType);
PopulateObject(element, property.GetRawText(), elementType);
}
addMethod.Invoke(target, new[] { element });
}
独特性
现在我们有一个问题。当前实现将始终添加到集合中,无论其当前内容如何。所以这将返回的东西既不是结果 1 也不是结果 2,而是结果 3:
{
"Title": "Startpage",
"Head": "Latest news"
"Links": [
{
"Id": 10,
"Text": "Start",
"Link": "/indexnews"
},
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More news",
"Link": "/morenews"
}
]
}
我们有一个包含链接 10 和 11 的数组,然后添加了另一个包含链接 11 和 12 的数组。没有明显的自然方式来处理这个问题。我在这里选择的设计决策是:集合决定元素是否已经存在。我们将Contains
在集合上调用默认方法并添加当且仅当它返回时false
。它要求我们重写Equals
方法Links
来比较Id
:
public override bool Equals(object? obj) =>
obj is Links other && Id == other.Id;
public override int GetHashCode() => Id.GetHashCode();
现在所需的更改是:
var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
var contains = containsMethod.Invoke(target, new[] { element });
if (contains is false)
{
addMethod.Invoke(target, new[] { element });
}
测试
Pages
我在你的和课堂上添加了一些东西Links
,首先我覆盖了ToString
,所以我们可以轻松地检查我们的结果。然后,如前所述,我覆盖Equals
:Links
public class Pages
{
public string Title { get; set; }
public string Head { get; set; }
public List<Links> Links { get; set; }
public override string ToString() =>
$"Pages {{ Title = {Title}, Head = {Head}, Links = {string.Join(", ", Links)} }}";
}
public class Links
{
public int Id { get; set; }
public string Text { get; set; }
public string Link { get; set; }
public override bool Equals(object? obj) =>
obj is Links other && Id == other.Id;
public override int GetHashCode() => Id.GetHashCode();
public override string ToString() => $"Links {{ Id = {Id}, Text = {Text}, Link = {Link} }}";
}
和测试:
var initial = @"{
""Title"": ""Startpage"",
""Links"": [
{
""Id"": 10,
""Text"": ""Start"",
""Link"": ""/index""
},
{
""Id"": 11,
""Text"": ""Info"",
""Link"": ""/info""
}
]
}";
var update = @"{
""Head"": ""Latest news"",
""Links"": [
{
""Id"": 11,
""Text"": ""News"",
""Link"": ""/news""
},
{
""Id"": 21,
""Text"": ""More News"",
""Link"": ""/morenews""
}
]
}";
var pages = new Pages();
PopulateObject(pages, initial);
Console.WriteLine(pages);
PopulateObject(pages, update);
Console.WriteLine(pages);
结果:
Initial:
Pages { Title = Startpage, Head = , Links = Links { Id = 10, Text = Start, Link = /index }, Links { Id = 11, Text = Info, Link = /info } }
Update:
Pages { Title = Startpage, Head = Latest news, Links = Links { Id = 10, Text = Start, Link = /index }, Links { Id = 11, Text = Info, Link = /info }, Links { Id = 21, Text = More News, Link = /morenews } }
你可以在这个 fiddle中找到它。
限制
- 我们使用该
Add
方法,因此这不适用于 .NET 数组的属性,因为您不能Add
使用它们。它们必须单独处理,首先创建元素,然后构造一个适当大小的数组并填充它。
- 使用的决定对
Contains
我来说有点不确定。最好能更好地控制添加到集合中的内容。但这很简单并且有效,因此对于 SO 答案来说就足够了。
最终代码
static class JsonUtils
{
public static void PopulateObject<T>(T target, string jsonSource) where T : class =>
PopulateObject(target, jsonSource, typeof(T));
public static void OverwriteProperty<T>(T target, JsonProperty updatedProperty) where T : class =>
OverwriteProperty(target, updatedProperty, typeof(T));
private static 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);
}
}
private static void PopulateCollection(object target, string jsonSource, Type elementType)
{
using var json = JsonDocument.Parse(jsonSource).RootElement;
var addMethod = target.GetType().GetMethod("Add", new[] { elementType });
var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
Debug.Assert(addMethod is not null);
Debug.Assert(containsMethod is not null);
foreach (var property in json.EnumerateArray())
{
object? element;
if (elementType.IsValueType || elementType == typeof(string))
{
element = JsonSerializer.Deserialize(jsonSource, elementType);
}
else if (IsCollection(elementType))
{
var nestedElementType = elementType.GenericTypeArguments[0];
element = Instantiate(elementType);
PopulateCollection(element, property.GetRawText(), nestedElementType);
}
else
{
element = Instantiate(elementType);
PopulateObject(element, property.GetRawText(), elementType);
}
var contains = containsMethod.Invoke(target, new[] { element });
if (contains is false)
{
addMethod.Invoke(target, new[] { element });
}
}
}
private static void OverwriteProperty(object target, JsonProperty updatedProperty, Type type)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
if (updatedProperty.Value.ValueKind == JsonValueKind.Null)
{
propertyInfo.SetValue(target, null);
return;
}
var propertyType = propertyInfo.PropertyType;
object? parsedValue;
if (propertyType.IsValueType || propertyType == typeof(string))
{
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else if (IsCollection(propertyType))
{
var elementType = propertyType.GenericTypeArguments[0];
parsedValue = propertyInfo.GetValue(target);
parsedValue ??= Instantiate(propertyType);
PopulateCollection(parsedValue, updatedProperty.Value.GetRawText(), elementType);
}
else
{
parsedValue = propertyInfo.GetValue(target);
parsedValue ??= Instantiate(propertyType);
PopulateObject(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
private static object Instantiate(Type type)
{
var ctor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Array.Empty<Type>());
if (ctor is null)
{
throw new InvalidOperationException($"Type {type.Name} has no parameterless constructor.");
}
return ctor.Invoke(Array.Empty<object?>());
}
private static bool IsCollection(Type type) =>
type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(ICollection<>));
}