.NET Core/System.Text.Json:枚举和添加/替换 json 属性/值

LGS*_*Son 10 c# system.text.json asp.net-core-6.0

在我之前的一个问题中,我询问如何使用 System.Text.Json填充现有对象

一个很好的答案展示了一种解决方案,用 解析 json 字符串JsonDocument并用 枚举它EnumerateObject

随着时间的推移,我的 json 字符串不断演变,现在还包含一个对象数组,当使用链接答案中的代码解析它时,它会抛出以下异常:

The requested operation requires an element of type 'Object', but the target element has type 'Array'.
Run Code Online (Sandbox Code Playgroud)

我发现人们可以以一种方式或另一种方式寻找JsonValueKind.Array, 并做这样的事情

if (json.ValueKind.Equals(JsonValueKind.Array))
{
    foreach (var item in json.EnumerateArray())
    {
        foreach (var property in item.EnumerateObject())
        {
            await OverwriteProperty(???);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但我无法做到这一点。

如何做到这一点并作为通用解决方案?

我想获得“结果 1”,其中添加/更新数组项,以及“结果 2”(当传递变量时),其中整个数组被替换。

对于“结果2”,我假设可以if (JsonValueKind.Array))OverwriteProperty方法中检测到,以及在哪里/如何传递“replaceArray”变量?...在迭代数组或对象时?

一些示例数据:

Json 字符串首字母

{
  "Title": "Startpage",
  "Links": [
    {
      "Id": 10,
      "Text": "Start",
      "Link": "/index"
    },
    {
      "Id": 11,
      "Text": "Info",
      "Link": "/info"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

要添加/更新的 Json 字符串

{
  "Head": "Latest news",
  "Links": [
    {
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    },
    {
      "Id": 21,
      "Text": "More News",
      "Link": "/morenews"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

结果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"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

结果2

{
  "Title": "Startpage",
  "Head": "Latest news"
  "Links": [
    {
      "Id": 11,
      "Text": "News",
      "Link": "/news"
    },
    {
      "Id": 21,
      "Text": "More News",
      "Link": "/morenews"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

课程

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; }
}
Run Code Online (Sandbox Code Playgroud)

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);
}
Run Code Online (Sandbox Code Playgroud)

V0l*_*dek 1

预赛

我将大量使用我对链接问题的回答中的现有代码:.Net Core 3.0 JsonSerializer populate existing object

正如我提到的,浅拷贝的代码可以工作并产生结果 2。因此我们只需要修复深拷贝的代码并让它产生结果 1。

PopulateObject在我的机器上,当propertyTypeis typeof(string),sincestring既不是值类型也不是 JSON 中的对象表示的东西时,代码就会崩溃。我在原来的答案中修复了这个问题,如果必须是:

if (elementType.IsValueType || elementType == typeof(string))
Run Code Online (Sandbox Code Playgroud)

落实新要求

好的,第一个问题是识别某个东西是否是集合。目前,我们会查看要覆盖的属性的类型来做出决定,因此现在我们将执行相同的操作。逻辑如下:

if (elementType.IsValueType || elementType == typeof(string))
Run Code Online (Sandbox Code Playgroud)

因此,我们唯一考虑集合的东西是ICollection<T>为某些实现的东西T。我们将通过实现一种新方法来完全单独地处理集合PopulateCollection。我们还需要一种方法来构造一个新集合 - 也许初始对象中的列表是null,因此我们需要在填充它之前创建一个新集合。为此,我们将寻找它的无参数构造函数:

private static bool IsCollection(Type type) =>
        type.GetInterfaces().Any(x => x.IsGenericType && 
        x.GetGenericTypeDefinition() == typeof(ICollection<>));
Run Code Online (Sandbox Code Playgroud)

我们允许它存在private,因为为什么不呢。

现在我们进行一些更改OverwriteProperty

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?>());
}
Run Code Online (Sandbox Code Playgroud)

最大的变化是声明的第二个分支if。我们找出集合中元素的类型并从对象中提取现有集合。如果它为空,我们创建一个新的空的。然后我们调用新方法来填充它。

PopulateCollection方法与 非常相似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);
    }
Run Code Online (Sandbox Code Playgroud)

首先我们获取Add集合的方法:

private static void PopulateCollection(object target, string jsonSource, Type elementType)
Run Code Online (Sandbox Code Playgroud)

这里我们期望一个实际的 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 });
}
Run Code Online (Sandbox Code Playgroud)

独特性

现在我们有一个问题。当前的实现将始终添加到集合中,无论其当前内容如何。所以返回的结果既不是 Result 1 也不是 Result 2,而是 Result 3:

var addMethod = target.GetType().GetMethod("Add", new[] { elementType });
Run Code Online (Sandbox Code Playgroud)

我们有一个带有链接 10 和 11 的数组,然后添加了另一个带有链接 11 和 12 的数组。没有明显的自然方法来处理这个问题。我在这里选择的设计决策是:集合决定元素是否已经存在。Contains我们将调用集合上的默认方法,并当且仅当它返回时添加false。它要求我们重写Equals方法来Links比较Id

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 });
}
Run Code Online (Sandbox Code Playgroud)

现在需要的更改是:

  • 首先,获取Contains方法:
var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
Run Code Online (Sandbox Code Playgroud)
  • 然后,在我们得到一个后检查它element
var contains = containsMethod.Invoke(target, new[] { element });
if (contains is false)
{
    addMethod.Invoke(target, new[] { element });
}
Run Code Online (Sandbox Code Playgroud)

测试

Pages我向您和班级添加了一些内容Links,首先我覆盖了ToString这样我们可以轻松检查我们的结果。然后,如前所述,我Equals覆盖Links

{
  "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"
    }
  ]
}
Run Code Online (Sandbox Code Playgroud)

和测试:

public override bool Equals(object? obj) =>
    obj is Links other && Id == other.Id;

public override int GetHashCode() => Id.GetHashCode();
Run Code Online (Sandbox Code Playgroud)

结果:

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 } }
Run Code Online (Sandbox Code Playgroud)

你可以在这个小提琴中找到它。

局限性

  1. 我们使用该Add方法,因此这不适用于 .NET 数组的属性,因为您无法Add操作它们。它们必须单独处理,首先创建元素,然后构造一个适当大小的数组并填充它。
  2. 使用的决定Contains对我来说有点不确定。如果能够更好地控制添加到集合中的内容,那就太好了。但这很简单并且有效,所以对于一个 SO 答案来说就足够了。

最终代码

var containsMethod = target.GetType().GetMethod("Contains", new[] { elementType });
Run Code Online (Sandbox Code Playgroud)