通过流化JSON的某些部分来解析庞大的OData JSON,以避免发生LOH

时间:2019-05-08 10:21:56

标签: c# json json.net odata odata.net

我有一个OData响应,如JSON 几MB ,并且要求是流式传输“ JSON的某些部分”,甚至不将其加载到内存中。

例如 :当我在下面的JSON(将以MB为单位)中读取属性“ value[0].Body.Content”时,我想流式传输此值部分,而无需将其反序列化为字符串类型的Object。因此,基本上将值部分读取到固定大小的字节数组中,然后将该字节数组写入目标流中(重复此步骤,直到完成数据处理为止)。

JSON:

{
    "@odata.context": "https://localhost:5555/api/v2.0/$metadata#Me/Messages",
    "value": [
        {
            "@odata.id": "https://localhost:5555/api/v2.0/",
            "@odata.etag": "W/\"Something\"",
            "Id": "vccvJHDSFds43hwy98fh",
            "CreatedDateTime": "2018-12-01T01:47:53Z",
            "LastModifiedDateTime": "2018-12-01T01:47:53Z",
            "ChangeKey": "SDgf43tsdf",
            "WebLink": "https://localhost:5555/?ItemID=dfsgsdfg9876ijhrf",
            "Body": {
                "ContentType": "HTML",
                "Content": "<html>\r\n<body>Huge Data Here\r\n</body>\r\n</html>\r\n"
            },
            "ToRecipients": [{
                    "EmailAddress": {
                        "Name": "ME",
                        "Address": "me@me.com"
                    }
                }
            ],
            "CcRecipients": [],
            "BccRecipients": [],
            "ReplyTo": [],
            "Flag": {
                "FlagStatus": "NotFlagged"
            }
        }
    ],
    "@odata.nextLink": "http://localhost:5555/rest/jersey/sleep?%24filter=LastDeliveredDateTime+ge+2018-12-01+and+LastDeliveredDateTime+lt+2018-12-02&%24top=50&%24skip=50"
}

尝试的方法:
 1. Newtonsoft

我最初尝试使用Newtonsoft流,但它internally converts the data into string and loads into memory。 (这会导致LOH激增,并且直到进行压缩时才会释放内存-我们对工作进程有内存限制,因此无法将其保留在内存中)

**code:**

    using (var jsonTextReader = new JsonTextReader(sr))
    {
        var pool = new CustomArrayPool();
        // Checking if pooling will help with memory
        jsonTextReader.ArrayPool = pool;

        while (jsonTextReader.Read())
        {
            if (jsonTextReader.TokenType == JsonToken.PropertyName
                && ((string)jsonTextReader.Value).Equals("value"))
            {
                jsonTextReader.Read();

                if (jsonTextReader.TokenType == JsonToken.StartArray)
                {
                    while (jsonTextReader.Read())
                    {
                        if (jsonTextReader.TokenType == JsonToken.StartObject)
                        {
                            var Current = JToken.Load(jsonTextReader);
                            // By Now, the LOH Shoots up.
                            // Avoid below code of converting this JToken back to byte array.
                            destinationStream.write(Encoding.ASCII.GetBytes(Current.ToString()));
                        }
                        else if (jsonTextReader.TokenType == JsonToken.EndArray)
                        {
                            break;
                        }
                    }
                }
            }

            if (jsonTextReader.TokenType == JsonToken.StartObject)
            {
                var Current = JToken.Load(jsonTextReader);
                // Do some processing with Current
                destinationStream.write(Encoding.ASCII.GetBytes(Current.ToString()));
            }
        }
    }
  1. OData.Net:

    我在考虑使用OData.Net库looks like it supports streaming of string fields是否可行。但这不能走太远,因为我最终为数据创建了一个模型,这意味着该值将转换为MB的一个字符串对象。

    代码

    ODataMessageReaderSettings settings = new ODataMessageReaderSettings();
    IODataResponseMessage responseMessage = new InMemoryMessage { Stream = stream };
    responseMessage.SetHeader("Content-Type", "application/json;odata.metadata=minimal;");
    // ODataMessageReader reader = new ODataMessageReader((IODataResponseMessage)message, settings, GetEdmModel());
    ODataMessageReader reader = new ODataMessageReader(responseMessage, settings, new EdmModel());
    var oDataResourceReader = reader.CreateODataResourceReader();
    var property = reader.ReadProperty();
    


有什么想法如何使用OData.Net/Newtonsoft和某些字段的流值来部分解析此JSON?
这样做的唯一方法是手动解析流吗?

1 个答案:

答案 0 :(得分:3)

如果要将JSON的一部分从一个流复制到另一个流,则可以使用JsonWriter.WriteToken(JsonReader)来更高效地执行此操作,从而避免了中间的Current = JToken.Load(jsonTextReader)Encoding.ASCII.GetBytes(Current.ToString())表示形式及其相关的内存开销:

using (var textWriter = new StreamWriter(destinationStream, new UTF8Encoding(false, true), 1024, true))
using (var jsonWriter = new JsonTextWriter(textWriter) { Formatting = Formatting.Indented, CloseOutput = false })
{
    // Use Formatting.Indented or Formatting.None as required.
    jsonWriter.WriteToken(jsonTextReader);
}

但是,Json.NET的JsonTextReader无法以与XmlReader.ReadValueChunk()相同的方式读取“块”中的单个字符串值。它将始终完全实现每个原子串值。如果您的字符串值太大而无法在大对象堆上使用,那么即使使用JsonWriter.WriteToken()也不会阻止这些字符串完全加载到内存中。

作为替代方案,您可以考虑由JsonReaderWriterFactory返回的读者和作家。 DataContractJsonSerializer使用这些读取器和写入器,将readwritten上的JSON即时转换为XML。由于这些读取器和写入器的基类为XmlReaderXmlWriter,因此它们 do 支持以块形式读取和写入字符串值。适当地使用它们将避免在大型对象堆中分配字符串。

为此,首先定义以下扩展方法,这些方法将JSON值的选定子集从输入流复制到输出流,如要流数据的路径所指定:

public static class JsonExtensions
{
    public static void StreamNested(Stream from, Stream to, string [] path)
    {
        var reversed = path.Reverse().ToArray();

        using (var xr = JsonReaderWriterFactory.CreateJsonReader(from, XmlDictionaryReaderQuotas.Max))
        {
            foreach (var subReader in xr.ReadSubtrees(s => s.Select(n => n.LocalName).SequenceEqual(reversed)))
            {
                using (var xw = JsonReaderWriterFactory.CreateJsonWriter(to, Encoding.UTF8, false))
                {
                    subReader.MoveToContent();

                    xw.WriteStartElement("root");
                    xw.WriteAttributes(subReader, true);

                    subReader.Read();

                    while (!subReader.EOF)
                    {
                        if (subReader.NodeType == XmlNodeType.Element && subReader.Depth == 1)
                            xw.WriteNode(subReader, true);
                        else
                            subReader.Read();
                    }

                    xw.WriteEndElement();
                }
            }
        }
    }
}

public static class XmlReaderExtensions
{
    public static IEnumerable<XmlReader> ReadSubtrees(this XmlReader xmlReader, Predicate<Stack<XName>> filter)
    {
        Stack<XName> names = new Stack<XName>();

        while (xmlReader.Read())
        {
            if (xmlReader.NodeType == XmlNodeType.Element)
            {
                names.Push(XName.Get(xmlReader.LocalName, xmlReader.NamespaceURI));
                if (filter(names))
                {
                    using (var subReader = xmlReader.ReadSubtree())
                    {
                        yield return subReader;
                    }
                }
            }

            if ((xmlReader.NodeType == XmlNodeType.Element && xmlReader.IsEmptyElement)
                || xmlReader.NodeType == XmlNodeType.EndElement)
            {
                names.Pop();
            }
        }
    }
}

现在,string [] path的{​​{1}}参数不是任何类型的路径。相反,它是一个路径,该路径与JsonReaderWriterFactory.CreateJsonReader()返回的StreamNested()转换后的XML元素的层次结构相对应,该XML元素对应于您要选择的JSON。用于此的映射反过来,翻译则由Microsoft在 Mapping Between JSON and XML 中记录。要仅选择和流传输与XmlReader匹配的JSON值,所需的XML路径为value[*]。因此,您可以通过执行以下操作选择并流式传输所需的嵌套对象:

//root/value/item

注意:

  • Mapping Between JSON and XML 有点复杂。使用以下扩展方法将一些示例JSON加载到JsonExtensions.StreamNested(inputStream, destinationStream, new[] { "root", "value", "item" }); 中通常会更容易:

    XDocument

    然后观察确定正确的XML路径。

  • 有关相关问题,请参见 JObject.SelectToken Equivalent in .NET