克隆一个 JsonNode 并将其附加到 .NET 6 中的另一个 JsonNode
Clone a JsonNode and attach it to another one in .NET 6
我在 .NET 6.0 中使用 System.Text.Json.Nodes
,我想做的很简单:从一个节点复制一个 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
然后代码 运行 很好,但是它 returns 所有节点而不是我指定的节点,这不是我想要的结果。另外由于代码运行良好,我认为直接将一个 JsonNode 设置为另一个 JsonNode 是可行的。
根据异常消息,似乎如果我想将一个 JsonNode 添加到另一个 JsonNode 上,我必须先将它从其父节点上拆开,但是我该怎么做呢?
请注意,我的 JSON 文件很大(超过 6MB),因此我想确保我的解决方案没有性能问题。
最简单的选择是将 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"
}
}
As JsonNode
has no Clone()
method as of .NET 6, the easiest way to copy it is probably to invoke the serializer's JsonSerializer.Deserialize<TValue>(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");
}
还有,你写了
since my json file is quite big (more than 6MB), I was worried there might be some performance issues.
在那种情况下,我会避免将 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 });
备注:
- Microsoft 的文档 explicitly recommends against serializing from and to strings instead of UTF-8 byte sequences for performance reasons 这是不将大型 JSON 文件加载到临时字符串缓冲区的另一个原因。
演示小提琴:
我在 .NET 6.0 中使用 System.Text.Json.Nodes
,我想做的很简单:从一个节点复制一个 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
然后代码 运行 很好,但是它 returns 所有节点而不是我指定的节点,这不是我想要的结果。另外由于代码运行良好,我认为直接将一个 JsonNode 设置为另一个 JsonNode 是可行的。
根据异常消息,似乎如果我想将一个 JsonNode 添加到另一个 JsonNode 上,我必须先将它从其父节点上拆开,但是我该怎么做呢?
请注意,我的 JSON 文件很大(超过 6MB),因此我想确保我的解决方案没有性能问题。
最简单的选择是将 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"
}
}
As JsonNode
has no Clone()
method as of .NET 6, the easiest way to copy it is probably to invoke the serializer's JsonSerializer.Deserialize<TValue>(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");
}
还有,你写了
since my json file is quite big (more than 6MB), I was worried there might be some performance issues.
在那种情况下,我会避免将 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 });
备注:
- Microsoft 的文档 explicitly recommends against serializing from and to strings instead of UTF-8 byte sequences for performance reasons 这是不将大型 JSON 文件加载到临时字符串缓冲区的另一个原因。
演示小提琴: