返回查询结果的异步流

时间:2014-05-30 14:13:23

标签: c# asp.net-web-api async-await ravendb

我有以下WebApi方法从RavenDB返回无限制的结果流:

public IEnumerable<Foo> Get()
{
    var query = DocumentSession.Query<Foo, FooIndex>();
    using (var enumerator = DocumentSession.Advanced.Stream(query))
        while (enumerator.MoveNext())
            yield return enumerator.Current.Document;
}

现在我想让那个异步。天真的方法当然不起作用:

public async Task<IEnumerable<Location>> Get()
{
    var query = AsyncDocumentSession.Query<Foo, FooIndex>();
    using (var enumerator = await AsyncDocumentSession.Advanced.StreamAsync(query))
        while (await enumerator.MoveNextAsync())
            yield return enumerator.Current.Document;
}

...因为该方法既不是异步又是迭代器。

5 个答案:

答案 0 :(得分:10)

由于这是一个WebAPI操作方法,因此HTTP会将您限制为单个响应。如果您只返回IEnumerable<T>,那么ASP.NET将在内存中枚举它,然后发送响应。

如果你对这个内存中的过程很好,那么你可以自己做同样的事情:

public async Task<List<Location>> Get()
{
  var result = new List<Location>();
  var query = AsyncDocumentSession.Query<Foo, FooIndex>();
  using (var enumerator = await AsyncDocumentSession.Advanced.StreamAsync(query))
    while (await enumerator.MoveNextAsync())
      result.Add(enumerator.Current.Document);
  return result;
}

但是,我认为最好使用流式响应,您可以通过PushStreamContent获得;像这样的东西:

public HttpResponseMessage Get()
{
  var query = AsyncDocumentSession.Query<Foo, FooIndex>();
  HttpResponseMessage response = Request.CreateResponse();
  response.Content = new PushStreamContent(
      async (stream, content, context) =>
      {
        using (stream)
        using (var enumerator = await AsyncDocumentSession.Advanced.StreamAsync(query))
        {
          while (await enumerator.MoveNextAsync())
          {
            // TODO: adjust encoding as necessary.
            var serialized = JsonConvert.SerializeObject(enumerator.CurrentDocument);
            var data = UTF8Encoding.UTF8.GetBytes(serialized);
            var countPrefix = BitConverter.GetBytes(data.Length);
            await stream.WriteAsync(countPrefix, 0, countPrefix.Length);
            await stream.WriteAsync(data, 0, data.Length);
          }
        }
      });
  return response;
}

流式响应不需要服务器将整个响应保存在内存中;但是,您必须决定将文档写入响应流的正确方法。上面的示例代码只是将它们转换为JSON,以UTF8编码,(二进制)长度 - 前缀这些字符串。

答案 1 :(得分:1)

您可以实现自己的迭代器,而不是让编译器为您生成一个迭代器。

但是,在迭代器上调用MoveNext也必须是异步的 - 这意味着你不能实现IEnumerable<T>`IEnumerator , you'd have to define your own interface, e.g., IAsyncEnumerator`。 而且你也无法在foreach循环中使用该迭代器。

我看到它的方式,你最好的办法是做StreamAsync所做的事情。创建一个自定义类型IAsyncEnumerable,返回实现自定义IAsyncEnumerator<T>方法的async T MoveNextAsync()。枚举将包装您的query对象,枚举器将获取文档会话的文档。

internal class AsyncDocumentEnumerable : IAsyncEnumerable<Document>
{
    private readonly YourQueryType _query;
    public AsyncDocumentEnumerable(YourQueryType query)
    {
        _query = query;
    }

    IAsyncEnumerator<Document> GetEnumerator()
    {
        return new AsyncDocumentEnumerator(_query);
    }
}


internal class AsyncDocumentEnumerator : IAsyncDocumentEnumerator<Document>
{
    private readonly YourQueryType _query;
    private IAsyncEnumerator<DocumentSession> _iter;

    public AsyncDocumentEnumerator(YourQueryType query)
    {
        _query = query;
    }

    public Task<bool> async MoveNextAsync()
    {
        if(_iter == null)
            _iter = await AsyncDocumentSession.Advanced.StreamAsync(query);

        bool moved = await _iter.MoveNextAsync();

        if(moved)
            Current = _iter.Current.Document;
        return moved;
    }

    public Document Current{get; private set;}
}

答案 2 :(得分:1)

在C#8中,他们引入了IAsyncEnumerable<int>

    async IAsyncEnumerable<int> GetBigResultsAsync()
    {
        await foreach (var result in GetResultsAsync())
        {
            if (result > 20) yield return result; 
        }
    }

答案 3 :(得分:0)

您可以查看ReactiveExtensions for .Net,它们是专为您的需求而设计的。最终结果可能如下所示:

public IObservable<Location> Get()
        {
            var locations = new Subject<Location>();

            Task.Run(() =>
                     {
                         var query = DocumentSession.Query<Foo, FooIndex>();
                         foreach (var document in DocumentSession.Advanced.Stream(query))
                         {
                             locations.OnNext(document);
                         }
                         locations.OnCompleted();
                     });

            return locations;
        }

答案 4 :(得分:0)

毕竟不是那么难。解决方案是一个格式化程序,它可以异步处理枚举器并将JSON写入流:

public class CustomJsonMediaTypeFormatter : JsonMediaTypeFormatter
{
    public override async Task WriteToStreamAsync(
           Type type, object value, Stream writeStream, HttpContent content,
           TransportContext transportContext, CancellationToken cancellationToken)
    {
        if (type.IsGenericType &&
            type.GetGenericTypeDefinition() == typeof(IAsyncEnumerator<>))
        {
            var writer = new JsonTextWriter(new StreamWriter(writeStream))
                         { CloseOutput = false };
            writer.WriteStartArray();
            await Serialize((dynamic)value, writer);
            writer.WriteEndArray();
            writer.Flush();
        }
        else
            await base.WriteToStreamAsync(type, value, writeStream, content,
                                          transportContext, cancellationToken);
    }

    async Task Serialize<T>(IAsyncEnumerator<StreamResult<T>> enumerator,
                            JsonTextWriter writer)
    {
        var serializer = JsonSerializer.Create(SerializerSettings);
        while (await enumerator.MoveNextAsync())
            serializer.Serialize(writer, enumerator.Current.Document);
    }
}

现在我的WebApi方法比以前更短了:

public Task<IAsyncEnumerator<StreamResult<Foo>>> Get()
{
    var query = AsyncDocumentSession.Query<Foo, FooIndex>();
    return AsyncDocumentSession.Advanced.StreamAsync(query);
}