.NET Core/System.Text.Json: 枚举和 add/replace json properties/values
.NET Core/System.Text.Json: Enumerate and add/replace json properties/values
在我之前的一个问题中,我问过如何 使用 System.Text.Json。
展示了一个用 JsonDocument
解析 json 字符串并用 EnumerateObject
.
枚举它的解决方案
随着时间的推移,我的 json 字符串发生了变化,现在也包含一个对象数组,当使用链接答案中的代码解析它时,它会抛出以下异常:
The requested operation requires an element of type 'Object', but the target element has type 'Array'.
我发现可以用一种或另一种方式寻找 JsonValueKind.Array
,然后做这样的事情
if (json.ValueKind.Equals(JsonValueKind.Array))
{
foreach (var item in json.EnumerateArray())
{
foreach (var property in item.EnumerateObject())
{
await OverwriteProperty(???);
}
}
}
但我做不到。
如何做到这一点,作为通用解决方案?
我想得到 "Result 1",其中数组项得到 added/updated,"Result 2" (传递变量时),整个数组被替换。
对于 "Result 2" 我假设可以在 OverwriteProperty
方法中检测到 if (JsonValueKind.Array))
,并且 where/how 可以通过 "replaceArray “ 多变的? ...在迭代数组或对象时?
一些示例数据:
Json 字符串首字母
{
"Title": "Startpage",
"Links": [
{
"Id": 10,
"Text": "Start",
"Link": "/index"
},
{
"Id": 11,
"Text": "Info",
"Link": "/info"
}
]
}
Json 字符串到 add/update
{
"Head": "Latest news",
"Links": [
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More News",
"Link": "/morenews"
}
]
}
结果 1
{
"Title": "Startpage",
"Head": "Latest news"
"Links": [
{
"Id": 10,
"Text": "Start",
"Link": "/indexnews"
},
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More news",
"Link": "/morenews"
}
]
}
结果 2
{
"Title": "Startpage",
"Head": "Latest news"
"Links": [
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More News",
"Link": "/morenews"
}
]
}
类
public class Pages
{
public string Title { get; set; }
public string Head { get; set; }
public List<Links> Links { get; set; }
}
public class Links
{
public int Id { get; set; }
public string Text { get; set; }
public string Link { get; set; }
}
C#代码:
public async Task PopulateObjectAsync(object target, string source, Type type, bool replaceArrays = false)
{
using var json = JsonDocument.Parse(source).RootElement;
if (json.ValueKind.Equals(JsonValueKind.Array))
{
foreach (var item in json.EnumerateArray())
{
foreach (var property in item.EnumerateObject())
{
await OverwriteProperty(???, replaceArray); //use "replaceArray" here ?
}
}
}
else
{
foreach (var property in json.EnumerateObject())
{
await OverwriteProperty(target, property, type, replaceArray); //use "replaceArray" here ?
}
}
return;
}
public async Task OverwriteProperty(object target, JsonProperty updatedProperty, Type type, bool replaceArrays)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType)
{
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else if (replaceArrays && "property is JsonValueKind.Array") //pseudo code sample
{
// use same code here as in above "IsValueType" ?
}
else
{
parsedValue = propertyInfo.GetValue(target);
await PopulateObjectAsync(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
经过进一步考虑,我认为更简单的替代解决方案应该是使用C#反射而不是依赖JSON。如果不能满足您的需求告诉我:
public class JsonPopulator
{
public static void PopulateObjectByReflection(object target, string json, bool replaceArray)
{
var type = target.GetType();
var replacements = JsonSerializer.Deserialize(json, type);
PopulateSubObject(target, replacements, replaceArray);
}
static void PopulateSubObject(object target, object? replacements, bool replaceArray)
{
if (replacements == null) { return; }
var props = target.GetType().GetProperties();
foreach (var prop in props)
{
// Skip if can't write
if (!prop.CanWrite) { continue; }
// Skip if no value in replacement
var propType = prop.PropertyType;
var replaceValue = prop.GetValue(replacements);
if (replaceValue == GetDefaultValue(propType)) { continue; }
// Now check if it's array AND we do not want to replace it
if (replaceValue is IEnumerable<object> replacementList)
{
var currList = prop.GetValue(target) as IEnumerable<object>;
var finalList = replaceValue;
// If there is no initial list, or if we simply want to replace the array
if (currList == null || replaceArray)
{
// Do nothing here, we simply replace it
}
else
{
// Append items at the end
finalList = currList.Concat(replacementList);
// Since casting logic is complicated, we use a trick to just
// Serialize then Deserialize it again
// At the cost of performance hit if it's too big
var listJson = JsonSerializer.Serialize(finalList);
finalList = JsonSerializer.Deserialize(listJson, propType);
}
prop.SetValue(target, finalList);
}
else if (propType.IsValueType || propType == typeof(string))
{
// Simply copy value over
prop.SetValue(target, replaceValue);
}
else
{
// Recursively copy child properties
var subTarget = prop.GetValue(target);
var subReplacement = prop.GetValue(replacements);
// Special case: if original object doesn't have the value
if (subTarget == null && subReplacement != null)
{
prop.SetValue(target, subReplacement);
}
else
{
PopulateSubObject(target, replacements, replaceArray);
}
}
}
}
// From
static object? GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
}
使用:
const string Json1 = "{\n \"Title\": \"Startpage\",\n \"Links\": [\n {\n \"Id\": 10,\n \"Text\": \"Start\",\n \"Link\": \"/index\"\n },\n {\n \"Id\": 11,\n \"Text\": \"Info\",\n \"Link\": \"/info\"\n }\n ]\n}";
const string Json2 = "{\n \"Head\": \"Latest news\",\n \"Links\": [\n {\n \"Id\": 11,\n \"Text\": \"News\",\n \"Link\": \"/news\"\n },\n {\n \"Id\": 21,\n \"Text\": \"More News\",\n \"Link\": \"/morenews\"\n }\n ]\n}";
var obj = JsonSerializer.Deserialize<Pages>(Json1)!;
JsonPopulator.PopulateObjectByReflection(obj, Json2, false);
Console.WriteLine(obj.Links.Count); // 4
JsonPopulator.PopulateObjectByReflection(obj, Json2, true);
Console.WriteLine(obj.Links.Count); // 2
当我将 List<Links>
替换为数组 Links[]
:
时,该解决方案甚至有效
public class Pages
{
// ...
public Links[] Links { get; set; }
}
JsonPopulator.PopulateObjectByReflection(obj, Json2, false);
Console.WriteLine(obj.Links.Length); // 4
JsonPopulator.PopulateObjectByReflection(obj, Json2, true);
Console.WriteLine(obj.Links.Length); // 2
废弃方案:
我认为一个简单的解决方案是包含父项及其当前 属性 信息。一个原因是并非每个 IEnumerable
无论如何都是可变的(例如数组),因此即使 replaceArray
为 false,您也会想要替换它。
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
const string Json1 = @"
{
""Bars"": [
{ ""Value"": 0 },
{ ""Value"": 1 }
]
}
";
const string Json2 = @"
{
""Bars"": [
{ ""Value"": 2 },
{ ""Value"": 3 }
]
}
";
var foo = JsonSerializer.Deserialize<Foo>(Json1)!;
PopulateObject(foo, Json2, false);
Console.WriteLine(foo.Bars.Count); // 4
PopulateObject(foo, Json2, true);
Console.WriteLine(foo.Bars.Count); // 2
static void PopulateObject(object target, string replacement, bool replaceArray)
{
using var doc = JsonDocument.Parse(Json2);
var root = doc.RootElement;
PopulateObjectWithJson(target, root, replaceArray, null, null);
}
static void PopulateObjectWithJson(object target, JsonElement el, bool replaceArray, object? parent, PropertyInfo? parentProp)
{
// There should be other checks
switch (el.ValueKind)
{
case JsonValueKind.Object:
// Just simple check here, you may want more logic
var props = target.GetType().GetProperties().ToDictionary(q => q.Name);
foreach (var jsonProp in el.EnumerateObject())
{
if (props.TryGetValue(jsonProp.Name, out var prop))
{
var subTarget = prop.GetValue(target);
// You may need to check for null etc here
ArgumentNullException.ThrowIfNull(subTarget);
PopulateObjectWithJson(subTarget, jsonProp.Value, replaceArray, target, prop);
}
}
break;
case JsonValueKind.Array:
var parsedItems = new List<object>();
foreach (var item in el.EnumerateArray())
{
// Parse your value here, I will just assume the type for simplicity
var bar = new Bar()
{
Value = item.GetProperty(nameof(Bar.Value)).GetInt32(),
};
parsedItems.Add(bar);
}
IEnumerable<object> finalItems = parsedItems;
if (!replaceArray)
{
finalItems = ((IEnumerable<object>)target).Concat(parsedItems);
}
// Parse your list into List/Array/Collection/etc
// You need reflection here as well
var list = finalItems.Cast<Bar>().ToList();
parentProp?.SetValue(parent, list);
break;
default:
// Should handle for other types
throw new NotImplementedException();
}
}
public class Foo
{
public List<Bar> Bars { get; set; } = null!;
}
public class Bar
{
public int Value { get; set; }
}
万一您想使用仅 JSON 的解决方案,尽管我认为它并不比反射解决方案好多少。它涵盖的用例绝对少于默认 JsonSerializer
,例如您可能遇到 IReadOnlyCollection
s.
的问题
public class JsonPopulator
{
public static void PopulateObject(object target, string json, bool replaceArray)
{
using var jsonDoc = JsonDocument.Parse(json);
var root = jsonDoc.RootElement;
// Simplify the process by making sure the first one is Object
if (root.ValueKind != JsonValueKind.Object)
{
throw new InvalidDataException("JSON Root must be a JSON Object");
}
var type = target.GetType();
foreach (var jsonProp in root.EnumerateObject())
{
var prop = type.GetProperty(jsonProp.Name);
if (prop == null || !prop.CanWrite) { continue; }
var currValue = prop.GetValue(target);
var value = ParseJsonValue(jsonProp.Value, prop.PropertyType, replaceArray, currValue);
if (value != null)
{
prop.SetValue(target, value);
}
}
}
static object? ParseJsonValue(JsonElement value, Type type, bool replaceArray, object? initialValue)
{
if (type.IsArray || type.IsAssignableTo(typeof(IEnumerable<object>)))
{
// Array or List
var initalArr = initialValue as IEnumerable<object>;
// Get the type of the Array/List element
var elType = GetElementType(type);
var parsingValues = new List<object?>();
foreach (var item in value.EnumerateArray())
{
parsingValues.Add(ParseJsonValue(item, elType, replaceArray, null));
}
List<object?> finalItems;
if (replaceArray || initalArr == null)
{
finalItems = parsingValues;
}
else
{
finalItems = initalArr.Concat(parsingValues).ToList();
}
// Cast them to the correct type
return CastIEnumrable(finalItems, type, elType);
}
else if (type.IsValueType || type == typeof(string))
{
// I don't think this is optimal but I will just use your code
// since I assume it is working for you
return JsonSerializer.Deserialize(
value.GetRawText(),
type);
}
else
{
// Assume it's object
// Assuming it's object
if (value.ValueKind != JsonValueKind.Object)
{
throw new InvalidDataException("Expecting a JSON object");
}
var finalValue = initialValue;
// If it's null, the original object didn't have it yet
// Initialize it using default constructor
// You may need to check for JsonConstructor as well
if (initialValue == null)
{
var constructor = type.GetConstructor(Array.Empty<Type>());
if (constructor == null)
{
throw new TypeAccessException($"{type.Name} does not have a default constructor.");
}
finalValue = constructor.Invoke(Array.Empty<object>());
}
foreach (var jsonProp in value.EnumerateObject())
{
var subProp = type.GetProperty(jsonProp.Name);
if (subProp == null || !subProp.CanWrite) { continue; }
var initialSubPropValue = subProp.GetValue(finalValue);
var finalSubPropValue = ParseJsonValue(jsonProp.Value, subProp.PropertyType, replaceArray, initialSubPropValue);
if (finalSubPropValue != null)
{
subProp.SetValue(finalValue, finalSubPropValue);
}
}
return finalValue;
}
}
static object? CastIEnumrable(List<object?> items, Type target, Type elementType)
{
object? result = null;
if (IsList(target))
{
if (target.IsInterface)
{
return items;
}
else
{
result = Activator.CreateInstance(target);
var col = (result as IList)!;
foreach (var item in items)
{
col.Add(item);
}
}
}
else if (target.IsArray)
{
result = Array.CreateInstance(elementType, items.Count);
var arr = (result as Array)!;
for (int i = 0; i < items.Count; i++)
{
arr.SetValue(items[i], i);
}
}
return result;
}
static bool IsList(Type type)
{
return type.GetInterface("IList") != null;
}
static Type GetElementType(Type enumerable)
{
return enumerable.GetInterfaces()
.First(q => q.IsGenericType && q.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.GetGenericArguments()[0];
}
}
用法:
const string Json1 = "{\n \"Title\": \"Startpage\",\n \"Links\": [\n {\n \"Id\": 10,\n \"Text\": \"Start\",\n \"Link\": \"/index\"\n },\n {\n \"Id\": 11,\n \"Text\": \"Info\",\n \"Link\": \"/info\"\n }\n ]\n}";
const string Json2 = "{\n \"Head\": \"Latest news\",\n \"Links\": [\n {\n \"Id\": 11,\n \"Text\": \"News\",\n \"Link\": \"/news\"\n },\n {\n \"Id\": 21,\n \"Text\": \"More News\",\n \"Link\": \"/morenews\"\n }\n ]\n}";
var obj = JsonSerializer.Deserialize<Pages>(Json1)!;
JsonPopulator.PopulateObject(obj, Json2, false);
Console.WriteLine(obj.Links.Count); // 4
Console.WriteLine(JsonSerializer.Serialize(obj));
JsonPopulator.PopulateObject(obj, Json2, true);
Console.WriteLine(obj.Links.Count); // 2
Console.WriteLine(JsonSerializer.Serialize(obj));
好吧,如果你不关心数组是怎么写的,我有一个简单的解决方案。在 2 个阶段中创建一个新的 JSON,1 个循环用于新属性,1 个循环用于更新:
var sourceJson = @"
{
""Title"": ""Startpage"",
""Links"": [
{
""Id"": 10,
""Text"": ""Start"",
""Link"": ""/index""
},
{
""Id"": 11,
""Text"": ""Info"",
""Link"": ""/info""
}
]
}";
var updateJson = @"
{
""Head"": ""Latest news"",
""Links"": [
{
""Id"": 11,
""Text"": ""News"",
""Link"": ""/news""
},
{
""Id"": 21,
""Text"": ""More News"",
""Link"": ""/morenews""
}
]
}
";
using var source = JsonDocument.Parse(sourceJson);
using var update = JsonDocument.Parse(updateJson);
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
writer.WriteStartObject();
// write non existing properties
foreach (var prop in update.RootElement.EnumerateObject().Where(prop => !source.RootElement.TryGetProperty(prop.Name, out _)))
{
prop.WriteTo(writer);
}
// make updates for existing
foreach (var prop in source.RootElement.EnumerateObject())
{
if (update.RootElement.TryGetProperty(prop.Name, out var overwrite))
{
writer.WritePropertyName(prop.Name);
overwrite.WriteTo(writer);
}
else
{
prop.WriteTo(writer);
}
}
writer.WriteEndObject();
writer.Flush();
var resultJson = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(resultJson);
输出:
{
"Head":"Latest news",
"Title":"Startpage",
"Links":[
{
"Id":11,
"Text":"News",
"Link":"/news"
},
{
"Id":21,
"Text":"More News",
"Link":"/morenews"
}
]
}
预赛
我将大量使用我对链接问题的回答中的现有代码:。
正如我提到的,浅拷贝的代码可以工作并产生结果 2。所以我们只需要修复深拷贝的代码并让它产生结果 1。
在我的机器上,当 propertyType
为 typeof(string)
时,代码在 PopulateObject
中崩溃,因为 string
既不是值类型,也不是 [ 中的对象表示的东西=122=]。我在原来的答案中修复了它,if 必须是:
if (elementType.IsValueType || elementType == typeof(string))
实施新要求
好的,第一个问题是识别某物是否是一个集合。目前我们查看要覆盖的 属性 的类型来做出决定,所以现在我们将做同样的事情。逻辑如下:
private static bool IsCollection(Type type) =>
type.GetInterfaces().Any(x => x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(ICollection<>));
所以我们唯一考虑集合的是为某些 T
实现 ICollection<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 数组,所以是时候枚举它了。对于数组中的每个元素,我们需要做与 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 });
}
唯一性
现在我们遇到了一个问题。当前的实现将总是 添加到集合中,无论其当前内容如何。所以这 return 既不是结果 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
方法,并当且仅当它 returns false
时添加。它需要我们覆盖 Links
上的 Equals
方法来比较 Id
:
public override bool Equals(object? obj) =>
obj is Links other && Id == other.Id;
public override int GetHashCode() => Id.GetHashCode();
现在需要的更改是:
- 首先,获取
Contains
方法:
var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
- 然后,得到一个
element
: 后检查一下
var contains = containsMethod.Invoke(target, new[] { element });
if (contains is false)
{
addMethod.Invoke(target, new[] { element });
}
测试
我向您的 Pages
和 Links
class 添加了一些东西,首先我覆盖了 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 } }
您可以在 this 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<>));
}
在我之前的一个问题中,我问过如何
JsonDocument
解析 json 字符串并用 EnumerateObject
.
随着时间的推移,我的 json 字符串发生了变化,现在也包含一个对象数组,当使用链接答案中的代码解析它时,它会抛出以下异常:
The requested operation requires an element of type 'Object', but the target element has type 'Array'.
我发现可以用一种或另一种方式寻找 JsonValueKind.Array
,然后做这样的事情
if (json.ValueKind.Equals(JsonValueKind.Array))
{
foreach (var item in json.EnumerateArray())
{
foreach (var property in item.EnumerateObject())
{
await OverwriteProperty(???);
}
}
}
但我做不到。
如何做到这一点,作为通用解决方案?
我想得到 "Result 1",其中数组项得到 added/updated,"Result 2" (传递变量时),整个数组被替换。
对于 "Result 2" 我假设可以在 OverwriteProperty
方法中检测到 if (JsonValueKind.Array))
,并且 where/how 可以通过 "replaceArray “ 多变的? ...在迭代数组或对象时?
一些示例数据:
Json 字符串首字母
{
"Title": "Startpage",
"Links": [
{
"Id": 10,
"Text": "Start",
"Link": "/index"
},
{
"Id": 11,
"Text": "Info",
"Link": "/info"
}
]
}
Json 字符串到 add/update
{
"Head": "Latest news",
"Links": [
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More News",
"Link": "/morenews"
}
]
}
结果 1
{
"Title": "Startpage",
"Head": "Latest news"
"Links": [
{
"Id": 10,
"Text": "Start",
"Link": "/indexnews"
},
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More news",
"Link": "/morenews"
}
]
}
结果 2
{
"Title": "Startpage",
"Head": "Latest news"
"Links": [
{
"Id": 11,
"Text": "News",
"Link": "/news"
},
{
"Id": 21,
"Text": "More News",
"Link": "/morenews"
}
]
}
类
public class Pages
{
public string Title { get; set; }
public string Head { get; set; }
public List<Links> Links { get; set; }
}
public class Links
{
public int Id { get; set; }
public string Text { get; set; }
public string Link { get; set; }
}
C#代码:
public async Task PopulateObjectAsync(object target, string source, Type type, bool replaceArrays = false)
{
using var json = JsonDocument.Parse(source).RootElement;
if (json.ValueKind.Equals(JsonValueKind.Array))
{
foreach (var item in json.EnumerateArray())
{
foreach (var property in item.EnumerateObject())
{
await OverwriteProperty(???, replaceArray); //use "replaceArray" here ?
}
}
}
else
{
foreach (var property in json.EnumerateObject())
{
await OverwriteProperty(target, property, type, replaceArray); //use "replaceArray" here ?
}
}
return;
}
public async Task OverwriteProperty(object target, JsonProperty updatedProperty, Type type, bool replaceArrays)
{
var propertyInfo = type.GetProperty(updatedProperty.Name);
if (propertyInfo == null)
{
return;
}
var propertyType = propertyInfo.PropertyType;
object parsedValue;
if (propertyType.IsValueType)
{
parsedValue = JsonSerializer.Deserialize(
updatedProperty.Value.GetRawText(),
propertyType);
}
else if (replaceArrays && "property is JsonValueKind.Array") //pseudo code sample
{
// use same code here as in above "IsValueType" ?
}
else
{
parsedValue = propertyInfo.GetValue(target);
await PopulateObjectAsync(
parsedValue,
updatedProperty.Value.GetRawText(),
propertyType);
}
propertyInfo.SetValue(target, parsedValue);
}
经过进一步考虑,我认为更简单的替代解决方案应该是使用C#反射而不是依赖JSON。如果不能满足您的需求告诉我:
public class JsonPopulator
{
public static void PopulateObjectByReflection(object target, string json, bool replaceArray)
{
var type = target.GetType();
var replacements = JsonSerializer.Deserialize(json, type);
PopulateSubObject(target, replacements, replaceArray);
}
static void PopulateSubObject(object target, object? replacements, bool replaceArray)
{
if (replacements == null) { return; }
var props = target.GetType().GetProperties();
foreach (var prop in props)
{
// Skip if can't write
if (!prop.CanWrite) { continue; }
// Skip if no value in replacement
var propType = prop.PropertyType;
var replaceValue = prop.GetValue(replacements);
if (replaceValue == GetDefaultValue(propType)) { continue; }
// Now check if it's array AND we do not want to replace it
if (replaceValue is IEnumerable<object> replacementList)
{
var currList = prop.GetValue(target) as IEnumerable<object>;
var finalList = replaceValue;
// If there is no initial list, or if we simply want to replace the array
if (currList == null || replaceArray)
{
// Do nothing here, we simply replace it
}
else
{
// Append items at the end
finalList = currList.Concat(replacementList);
// Since casting logic is complicated, we use a trick to just
// Serialize then Deserialize it again
// At the cost of performance hit if it's too big
var listJson = JsonSerializer.Serialize(finalList);
finalList = JsonSerializer.Deserialize(listJson, propType);
}
prop.SetValue(target, finalList);
}
else if (propType.IsValueType || propType == typeof(string))
{
// Simply copy value over
prop.SetValue(target, replaceValue);
}
else
{
// Recursively copy child properties
var subTarget = prop.GetValue(target);
var subReplacement = prop.GetValue(replacements);
// Special case: if original object doesn't have the value
if (subTarget == null && subReplacement != null)
{
prop.SetValue(target, subReplacement);
}
else
{
PopulateSubObject(target, replacements, replaceArray);
}
}
}
}
// From
static object? GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
}
使用:
const string Json1 = "{\n \"Title\": \"Startpage\",\n \"Links\": [\n {\n \"Id\": 10,\n \"Text\": \"Start\",\n \"Link\": \"/index\"\n },\n {\n \"Id\": 11,\n \"Text\": \"Info\",\n \"Link\": \"/info\"\n }\n ]\n}";
const string Json2 = "{\n \"Head\": \"Latest news\",\n \"Links\": [\n {\n \"Id\": 11,\n \"Text\": \"News\",\n \"Link\": \"/news\"\n },\n {\n \"Id\": 21,\n \"Text\": \"More News\",\n \"Link\": \"/morenews\"\n }\n ]\n}";
var obj = JsonSerializer.Deserialize<Pages>(Json1)!;
JsonPopulator.PopulateObjectByReflection(obj, Json2, false);
Console.WriteLine(obj.Links.Count); // 4
JsonPopulator.PopulateObjectByReflection(obj, Json2, true);
Console.WriteLine(obj.Links.Count); // 2
当我将 List<Links>
替换为数组 Links[]
:
public class Pages
{
// ...
public Links[] Links { get; set; }
}
JsonPopulator.PopulateObjectByReflection(obj, Json2, false);
Console.WriteLine(obj.Links.Length); // 4
JsonPopulator.PopulateObjectByReflection(obj, Json2, true);
Console.WriteLine(obj.Links.Length); // 2
废弃方案:
我认为一个简单的解决方案是包含父项及其当前 属性 信息。一个原因是并非每个 IEnumerable
无论如何都是可变的(例如数组),因此即使 replaceArray
为 false,您也会想要替换它。
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.Json;
const string Json1 = @"
{
""Bars"": [
{ ""Value"": 0 },
{ ""Value"": 1 }
]
}
";
const string Json2 = @"
{
""Bars"": [
{ ""Value"": 2 },
{ ""Value"": 3 }
]
}
";
var foo = JsonSerializer.Deserialize<Foo>(Json1)!;
PopulateObject(foo, Json2, false);
Console.WriteLine(foo.Bars.Count); // 4
PopulateObject(foo, Json2, true);
Console.WriteLine(foo.Bars.Count); // 2
static void PopulateObject(object target, string replacement, bool replaceArray)
{
using var doc = JsonDocument.Parse(Json2);
var root = doc.RootElement;
PopulateObjectWithJson(target, root, replaceArray, null, null);
}
static void PopulateObjectWithJson(object target, JsonElement el, bool replaceArray, object? parent, PropertyInfo? parentProp)
{
// There should be other checks
switch (el.ValueKind)
{
case JsonValueKind.Object:
// Just simple check here, you may want more logic
var props = target.GetType().GetProperties().ToDictionary(q => q.Name);
foreach (var jsonProp in el.EnumerateObject())
{
if (props.TryGetValue(jsonProp.Name, out var prop))
{
var subTarget = prop.GetValue(target);
// You may need to check for null etc here
ArgumentNullException.ThrowIfNull(subTarget);
PopulateObjectWithJson(subTarget, jsonProp.Value, replaceArray, target, prop);
}
}
break;
case JsonValueKind.Array:
var parsedItems = new List<object>();
foreach (var item in el.EnumerateArray())
{
// Parse your value here, I will just assume the type for simplicity
var bar = new Bar()
{
Value = item.GetProperty(nameof(Bar.Value)).GetInt32(),
};
parsedItems.Add(bar);
}
IEnumerable<object> finalItems = parsedItems;
if (!replaceArray)
{
finalItems = ((IEnumerable<object>)target).Concat(parsedItems);
}
// Parse your list into List/Array/Collection/etc
// You need reflection here as well
var list = finalItems.Cast<Bar>().ToList();
parentProp?.SetValue(parent, list);
break;
default:
// Should handle for other types
throw new NotImplementedException();
}
}
public class Foo
{
public List<Bar> Bars { get; set; } = null!;
}
public class Bar
{
public int Value { get; set; }
}
万一您想使用仅 JSON 的解决方案,尽管我认为它并不比反射解决方案好多少。它涵盖的用例绝对少于默认 JsonSerializer
,例如您可能遇到 IReadOnlyCollection
s.
public class JsonPopulator
{
public static void PopulateObject(object target, string json, bool replaceArray)
{
using var jsonDoc = JsonDocument.Parse(json);
var root = jsonDoc.RootElement;
// Simplify the process by making sure the first one is Object
if (root.ValueKind != JsonValueKind.Object)
{
throw new InvalidDataException("JSON Root must be a JSON Object");
}
var type = target.GetType();
foreach (var jsonProp in root.EnumerateObject())
{
var prop = type.GetProperty(jsonProp.Name);
if (prop == null || !prop.CanWrite) { continue; }
var currValue = prop.GetValue(target);
var value = ParseJsonValue(jsonProp.Value, prop.PropertyType, replaceArray, currValue);
if (value != null)
{
prop.SetValue(target, value);
}
}
}
static object? ParseJsonValue(JsonElement value, Type type, bool replaceArray, object? initialValue)
{
if (type.IsArray || type.IsAssignableTo(typeof(IEnumerable<object>)))
{
// Array or List
var initalArr = initialValue as IEnumerable<object>;
// Get the type of the Array/List element
var elType = GetElementType(type);
var parsingValues = new List<object?>();
foreach (var item in value.EnumerateArray())
{
parsingValues.Add(ParseJsonValue(item, elType, replaceArray, null));
}
List<object?> finalItems;
if (replaceArray || initalArr == null)
{
finalItems = parsingValues;
}
else
{
finalItems = initalArr.Concat(parsingValues).ToList();
}
// Cast them to the correct type
return CastIEnumrable(finalItems, type, elType);
}
else if (type.IsValueType || type == typeof(string))
{
// I don't think this is optimal but I will just use your code
// since I assume it is working for you
return JsonSerializer.Deserialize(
value.GetRawText(),
type);
}
else
{
// Assume it's object
// Assuming it's object
if (value.ValueKind != JsonValueKind.Object)
{
throw new InvalidDataException("Expecting a JSON object");
}
var finalValue = initialValue;
// If it's null, the original object didn't have it yet
// Initialize it using default constructor
// You may need to check for JsonConstructor as well
if (initialValue == null)
{
var constructor = type.GetConstructor(Array.Empty<Type>());
if (constructor == null)
{
throw new TypeAccessException($"{type.Name} does not have a default constructor.");
}
finalValue = constructor.Invoke(Array.Empty<object>());
}
foreach (var jsonProp in value.EnumerateObject())
{
var subProp = type.GetProperty(jsonProp.Name);
if (subProp == null || !subProp.CanWrite) { continue; }
var initialSubPropValue = subProp.GetValue(finalValue);
var finalSubPropValue = ParseJsonValue(jsonProp.Value, subProp.PropertyType, replaceArray, initialSubPropValue);
if (finalSubPropValue != null)
{
subProp.SetValue(finalValue, finalSubPropValue);
}
}
return finalValue;
}
}
static object? CastIEnumrable(List<object?> items, Type target, Type elementType)
{
object? result = null;
if (IsList(target))
{
if (target.IsInterface)
{
return items;
}
else
{
result = Activator.CreateInstance(target);
var col = (result as IList)!;
foreach (var item in items)
{
col.Add(item);
}
}
}
else if (target.IsArray)
{
result = Array.CreateInstance(elementType, items.Count);
var arr = (result as Array)!;
for (int i = 0; i < items.Count; i++)
{
arr.SetValue(items[i], i);
}
}
return result;
}
static bool IsList(Type type)
{
return type.GetInterface("IList") != null;
}
static Type GetElementType(Type enumerable)
{
return enumerable.GetInterfaces()
.First(q => q.IsGenericType && q.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.GetGenericArguments()[0];
}
}
用法:
const string Json1 = "{\n \"Title\": \"Startpage\",\n \"Links\": [\n {\n \"Id\": 10,\n \"Text\": \"Start\",\n \"Link\": \"/index\"\n },\n {\n \"Id\": 11,\n \"Text\": \"Info\",\n \"Link\": \"/info\"\n }\n ]\n}";
const string Json2 = "{\n \"Head\": \"Latest news\",\n \"Links\": [\n {\n \"Id\": 11,\n \"Text\": \"News\",\n \"Link\": \"/news\"\n },\n {\n \"Id\": 21,\n \"Text\": \"More News\",\n \"Link\": \"/morenews\"\n }\n ]\n}";
var obj = JsonSerializer.Deserialize<Pages>(Json1)!;
JsonPopulator.PopulateObject(obj, Json2, false);
Console.WriteLine(obj.Links.Count); // 4
Console.WriteLine(JsonSerializer.Serialize(obj));
JsonPopulator.PopulateObject(obj, Json2, true);
Console.WriteLine(obj.Links.Count); // 2
Console.WriteLine(JsonSerializer.Serialize(obj));
好吧,如果你不关心数组是怎么写的,我有一个简单的解决方案。在 2 个阶段中创建一个新的 JSON,1 个循环用于新属性,1 个循环用于更新:
var sourceJson = @"
{
""Title"": ""Startpage"",
""Links"": [
{
""Id"": 10,
""Text"": ""Start"",
""Link"": ""/index""
},
{
""Id"": 11,
""Text"": ""Info"",
""Link"": ""/info""
}
]
}";
var updateJson = @"
{
""Head"": ""Latest news"",
""Links"": [
{
""Id"": 11,
""Text"": ""News"",
""Link"": ""/news""
},
{
""Id"": 21,
""Text"": ""More News"",
""Link"": ""/morenews""
}
]
}
";
using var source = JsonDocument.Parse(sourceJson);
using var update = JsonDocument.Parse(updateJson);
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
writer.WriteStartObject();
// write non existing properties
foreach (var prop in update.RootElement.EnumerateObject().Where(prop => !source.RootElement.TryGetProperty(prop.Name, out _)))
{
prop.WriteTo(writer);
}
// make updates for existing
foreach (var prop in source.RootElement.EnumerateObject())
{
if (update.RootElement.TryGetProperty(prop.Name, out var overwrite))
{
writer.WritePropertyName(prop.Name);
overwrite.WriteTo(writer);
}
else
{
prop.WriteTo(writer);
}
}
writer.WriteEndObject();
writer.Flush();
var resultJson = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(resultJson);
输出:
{
"Head":"Latest news",
"Title":"Startpage",
"Links":[
{
"Id":11,
"Text":"News",
"Link":"/news"
},
{
"Id":21,
"Text":"More News",
"Link":"/morenews"
}
]
}
预赛
我将大量使用我对链接问题的回答中的现有代码:
正如我提到的,浅拷贝的代码可以工作并产生结果 2。所以我们只需要修复深拷贝的代码并让它产生结果 1。
在我的机器上,当 propertyType
为 typeof(string)
时,代码在 PopulateObject
中崩溃,因为 string
既不是值类型,也不是 [ 中的对象表示的东西=122=]。我在原来的答案中修复了它,if 必须是:
if (elementType.IsValueType || elementType == typeof(string))
实施新要求
好的,第一个问题是识别某物是否是一个集合。目前我们查看要覆盖的 属性 的类型来做出决定,所以现在我们将做同样的事情。逻辑如下:
private static bool IsCollection(Type type) =>
type.GetInterfaces().Any(x => x.IsGenericType &&
x.GetGenericTypeDefinition() == typeof(ICollection<>));
所以我们唯一考虑集合的是为某些 T
实现 ICollection<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 数组,所以是时候枚举它了。对于数组中的每个元素,我们需要做与 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 });
}
唯一性
现在我们遇到了一个问题。当前的实现将总是 添加到集合中,无论其当前内容如何。所以这 return 既不是结果 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
方法,并当且仅当它 returns false
时添加。它需要我们覆盖 Links
上的 Equals
方法来比较 Id
:
public override bool Equals(object? obj) =>
obj is Links other && Id == other.Id;
public override int GetHashCode() => Id.GetHashCode();
现在需要的更改是:
- 首先,获取
Contains
方法:
var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
- 然后,得到一个
element
: 后检查一下
var contains = containsMethod.Invoke(target, new[] { element });
if (contains is false)
{
addMethod.Invoke(target, new[] { element });
}
测试
我向您的 Pages
和 Links
class 添加了一些东西,首先我覆盖了 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 } }
您可以在 this 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<>));
}