ASP.NET Core 对嵌套属性进行 IAsyncEnumerate 流式处理

ASP.NET Core stream an IAsyncEnumerate on nested property

提问人:GLuca74 提问时间:7/24/2022 最后编辑:JackdawGLuca74 更新时间:12/23/2022 访问量:620

问:

我已经阅读了有关返回以利用 HTTP 流的信息。 如果我返回一个作为顶级,这将起作用。 因此,在客户端中:IAsyncEnumerate<Entity>IAsyncEnumerate<Entity>

Stream responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
IAsyncEnumerable<Entity> entities = JsonSerializer.DeserializeAsyncEnumerable<Entity>(
    responseStream,
    new JsonSerializerOptions
    {
        PropertyNameCaseInsensitive = true,
        DefaultBufferSize = 128
    });

await foreach (Entity entity in entities)
{
    ...
}

按预期工作。

就我而言,我的服务器返回一个类,其中 是一个嵌套属性:IAsyncEnumerate<Entity>

public class ServerResponse
{
    public int Property1 {get; set;}
    public IAsyncEnumerate<Entity> Entities {get; set;}
}

在这种情况下,我能否获得在客户端进行流式传输的优势?

C# ASP.NET-Core HTTP-Streaming IAsyncenumerable

评论


答:

0赞 M. Oberauer 7/24/2022 #1

文档

将 UTF-8 编码的文本包装到 IAsyncEnumerable 中,该文本可用于以流式处理方式反序列化根级 JSON 数组

所以不,因为它不是根级,不幸的是它不会起作用。

评论

0赞 M. Oberauer 7/24/2022
我关于解决此问题的建议是返回具有属性的实体以及指向端点的链接,该端点确实返回根级数组。如果你愿意的话,HATEOAS 方式。
1赞 Luke Oščádal 12/23/2022 #2

如果您只对要反序列化的数组中的数据感兴趣,并且想要忽略其他数据,那么有一种方法可以实现这一点。您可以编写一个自定义项,该自定义项将在反序列化期间走私出已反序列化的项。IAsyncEnumerableJsonConverter

下面是一个示例:

public static class Deserializer
{
    public static IAsyncEnumerable<TDataItem> DeserializeAsyncEnumerable<TDataItem>(Stream stream, string propertyPath)
    {
        var converter = new AsyncEnumerableConverter<TDataItem>(propertyPath);
        _ = JsonSerializer.DeserializeAsync<object>(stream, new JsonSerializerOptions
        {
            Converters = { converter }
        });

        return converter.OutputChannel.Reader.ReadAllAsync();
    }

    private class AsyncEnumerableConverter<TDataItem> : JsonConverter<object>
    {
        private readonly string _propertyPath;

        public AsyncEnumerableConverter(string propertyPath)
        {
            _propertyPath = propertyPath;
        }

        public Channel<TDataItem> OutputChannel { get; } = Channel.CreateUnbounded<TDataItem>(new()
        {
            SingleReader = true,
            SingleWriter = true
        });

        public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if (!TryMoveToItemsProperty(ref reader))
            {
                OutputChannel.Writer.Complete();
                return null;
            }

            if (reader.TokenType == JsonTokenType.Null)
            {
                OutputChannel.Writer.Complete();
                return null;
            }

            if (reader.TokenType != JsonTokenType.StartArray)
            {
                throw new JsonException($"Property {_propertyPath} is not JSON Array");
            }

            reader.Read(); // read start array
            ReadItems(ref reader, options);

            OutputChannel.Writer.Complete();
            return null;
        }

        public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) =>
            throw new NotSupportedException();

        private bool TryMoveToItemsProperty(ref Utf8JsonReader reader)
        {
            var propertyNames = _propertyPath.Split('.');
            var level = 0;

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.EndObject)
                {
                    return false;
                }

                if (reader.TokenType == JsonTokenType.PropertyName &&
                    reader.GetString() == propertyNames[level])
                {
                    level++;
                }

                if (level == propertyNames.Length)
                {
                    reader.Read();
                    return true;
                }
            }

            throw new JsonException("Invalid JSON");
        }

        private void ReadItems(ref Utf8JsonReader reader, JsonSerializerOptions options)
        {
            while (reader.TokenType != JsonTokenType.EndArray)
            {
                var item = JsonSerializer.Deserialize<TDataItem>(ref reader, options);    
                OutputChannel.Writer.TryWrite(item);
                reader.Read();
            }
        }
    }
}

转换器会跳过您不感兴趣的所有数据,并根据传递到数组的路径进行导航。在反序列化所有元素并将它们写入通道后,它不会继续读取。(当然,可以对最终返回的所有数据进行完全反序列化,但在这种情况下,这对我来说没有意义)

该方法创建转换器的实例,并开始使用它进行反序列化,但不会等待它完成,而是直接返回转换器的输出通道。此外,该方法在内部使用,以确保所需的内存效率。DeserializeAsyncEnumerableIAsyncEnumerableDeserializeAsync(Stream)

请考虑以下数据模型:

public class Root
{
    public string Property { get; set; }
    public DataBox DataBox { get; set; }
}

public class DataBox
{
    public string Property { get; set; }
    public ItemsBox ItemsBox { get; set; }
}

public class ItemsBox
{
    public string Property { get; set; }
    public List<Item> Items { get; set; }
}

public class Item
{
    public Guid Id { get; set; }
    public string Property { get; set; }
}

那么用法如下:

var asyncEnumerable = Deserializer.DeserializeAsyncEnumerable<Item>(stream, "DataBox.ItemsBox.Items");

await foreach (var dataItem in asyncEnumerable)
{
    ...
}