【问题标题】:Clone a JsonNode and attach it to another one in .NET 6克隆一个 JsonNode 并将其附加到 .NET 6 中的另一个
【发布时间】:2022-07-28 01:32:03
【问题描述】:

我在 .NET 6.0 中使用 System.Text.Json.Nodes,我想做的很简单:从一个 JsonNode 复制一个 JsonNode 并将该节点附加到另一个 JsonNode。
以下是我的代码。

public static string concQuest(string input, string allQuest, string questId) {
    JsonNode inputNode = JsonNode.Parse(input)!;
    JsonNode allQuestNode = JsonNode.Parse(allQuest)!;
    JsonNode quest = allQuestNode.AsArray().First(quest => 
        quest!["id"]!.GetValue<string>() == questId) ?? throw new KeyNotFoundException("No matching questId found.");
    inputNode["quest"] = quest;  // Exception occured
    return inputNode.ToJsonString(options);
}

但是当我尝试运行它时,我得到了一个System.InvalidOperationException 说"The node already has a parent."

我试过编辑

inputNode["quest"] = quest;

到

inputNode["quest"] = quest.Root; // quest.Root is also a JsonNode

然后代码运行良好,但它返回所有节点而不是我指定的节点,这不是我想要的结果。此外,由于代码运行良好,我认为直接将 JsonNode 设置为另一个是可行的。
根据异常消息,如果我想将一个 JsonNode 添加到另一个 JsonNode,我必须先将它从其父级中取消附加,但我该怎么做呢?

请注意,我的 JSON 文件非常大(超过 6MB),因此我想确保我的解决方案没有性能问题。

【问题讨论】:

  • 你的 JSON 是什么样的?可以分享minimal reproducible example吗?
  • “我想确保我的解决方案没有性能问题。” - 如果你想确保没有问题,你需要在测试中进行一些实施它是针对真实数据的表现。实际上,在很多情况下,“足够好”是一种无需过早优化的方法。

标签: c# json .net .net-6.0 system.text.json


【解决方案1】:

最简单的选择是将 json 节点转换为字符串并再次解析它(尽管可能不是性能最高的):

var destination = @"{}";
var source = "[{\"id\": 1, \"name\":\"some quest\"},{}]";
var sourceJson = JsonNode.Parse(source);
var destinationJson = JsonNode.Parse(destination);
var quest = sourceJson.AsArray().First();
destinationJson["quest"] = JsonNode.Parse(quest.ToJsonString());
Console.WriteLine(destinationJson.ToJsonString(new() { WriteIndented = true }));

将打印:

{
  "quest": {
    "id": 1,
    "name": "some quest"
  }
}

UPD

另一个技巧是将JsonNode反序列化为JsonNode:

...
var quest = sourceJson.AsArray().First();    
var clone = quest.Deserialize<JsonNode>();
clone["name"] = "New name"; 
destinationJson["quest"] = clone;
Console.WriteLine(quest["name"]);
Console.WriteLine(destinationJson.ToJsonString(new() { WriteIndented = true }));

打印:

some quest
{
  "quest": {
    "id": 1,
    "name": "New name"
  }
}

【讨论】:

  • 是的,我也考虑过这个解决方案。但是,由于我的 json 文件很大(超过 6MB),我担心可能会出现一些性能问题。
  • @Vibbit 6 mb 是一个任务的大小?
【解决方案2】:

由于JsonNode 在.NET 6 中没有Clone() 方法,因此复制它的最简单方法可能是调用序列化程序的JsonSerializer.Deserialize&lt;TValue&gt;(JsonNode, JsonSerializerOptions) 扩展方法将您的节点直接反序列化到另一个节点。首先,引入以下扩展方法来复制或移动节点:

public static partial class JsonExtensions
{
    public static TNode? CopyNode<TNode>(this TNode? node) where TNode : JsonNode => node?.Deserialize<TNode>();

    public static JsonNode? MoveNode(this JsonArray array, int id, JsonObject newParent, string name)
    {
        var node = array[id];
        array.RemoveAt(id); 
        return newParent[name] = node;
    }

    public static JsonNode? MoveNode(this JsonObject parent, string oldName, JsonObject newParent, string name)
    {
        parent.Remove(oldName, out var node);
        return newParent[name] = node;
    }

    public static TNode ThrowOnNull<TNode>(this TNode? value) where TNode : JsonNode => value ?? throw new JsonException("Null JSON value");
}

现在你的代码可以写成如下:

public static string concQuest(string input, string allQuest, string questId) 
{
    var inputObject = JsonNode.Parse(input).ThrowOnNull().AsObject();
    var allQuestArray = JsonNode.Parse(allQuest).ThrowOnNull().AsArray();
    concQuest(inputObject, allQuestArray, questId);
    return inputObject.ToJsonString();
}       

public static JsonNode? concQuest(JsonObject inputObject, JsonArray allQuestArray, string questId) 
{
    // Enumerable.First() will throw an InvalidOperationException if no element is found satisfying the predicate.
    var node = allQuestArray.First(quest => quest!["id"]!.GetValue<string>() == questId);
    return inputObject["quest"] = node.CopyNode();
}

或者,如果你不打算保留你的任务数组,你可以像这样将节点从数组移动到目标:

public static string concQuest(string input, string allQuest, string questId) 
{
    var inputObject = JsonNode.Parse(input).ThrowOnNull().AsObject();
    var allQuestArray = JsonNode.Parse(allQuest).ThrowOnNull().AsArray();
    concQuest(inputObject, allQuestArray, questId);
    return inputObject.ToJsonString();
}       

public static JsonNode? concQuest(JsonObject inputObject, JsonArray allQuestArray, string questId) 
{
    // Enumerable.First() will throw an InvalidOperationException if no element is found satisfying the predicate.
    var (_, index) = allQuestArray.Select((quest, index) => (quest, index)).First(p => p.quest!["id"]!.GetValue<string>() == questId);
    return allQuestArray.MoveNode(index, inputObject, "quest");
}

你也写了

由于我的 json 文件很大(超过 6MB),我担心可能会出现一些性能问题。

在这种情况下,我会避免将 JSON 文件加载到 input 和 allQuest 字符串中,因为大于 85,000 字节的字符串会出现在 large object heap 上,这可能会导致后续性能下降。相反,直接从相关文件反序列化为JsonNode 数组和对象,如下所示:

var questId = "2"; // Or whatever

JsonArray allQuest;
using (var stream = new FileStream(allQuestFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read }))
    allQuest = JsonNode.Parse(stream).ThrowOnNull().AsArray();

JsonObject input;
using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read }))
    input = JsonNode.Parse(stream).ThrowOnNull().AsObject();

JsonExtensions.concQuest(input, allQuest, questId);

using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Create, Access = FileAccess.Write }))
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true }))
    input.WriteTo(writer);

或者,如果您的应用是异步的,您可以这样做:

JsonArray allQuest;
await using (var stream = new FileStream(allQuestFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read, Options = FileOptions.Asynchronous }))
    allQuest = (await JsonSerializer.DeserializeAsync<JsonArray>(stream)).ThrowOnNull();

JsonObject input;
await using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Open, Access = FileAccess.Read, Options = FileOptions.Asynchronous }))
    input = (await JsonSerializer.DeserializeAsync<JsonObject>(stream)).ThrowOnNull();

JsonExtensions.concQuest(input, allQuest, questId);

await using (var stream = new FileStream(inputFileName, new FileStreamOptions { Mode = FileMode.Create, Access = FileAccess.Write, Options = FileOptions.Asynchronous }))
    await JsonSerializer.SerializeAsync(stream, input, new JsonSerializerOptions { WriteIndented = true });

注意事项:

演示小提琴:

【讨论】:

  • 非常感谢您的回答和建议,我认为这可能是目前最好的解决方案。还有一件事我很好奇,如果 JsonNode 已经有父节点,为什么我们不能直接将它复制到另一个节点?如果我们这样做有什么问题?
  • @Vibbit - 我不知道为什么 MSFT 会这样设计他们的 API。使用 Json.NET,当您将具有父节点的节点分配给另一个节点时,该节点会自动克隆,请参阅nested json objects dont update / inherit using Json.NET。或许 MSFT 的 API 设计人员不喜欢这样,所以他们决定抛出异常作为最简单的做法。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-06-19
  • 2021-09-08
  • 2014-04-29
  • 1970-01-01
  • 2014-07-03
  • 2011-12-23
  • 1970-01-01
相关资源
最近更新 更多