Как вести потоковую передачу с ASP.NET Core

Как правильно транслировать ответ в ASP.NET Core? Вот такой контроллер (ОБНОВЛЕННЫЙ КОД):

[HttpGet("test")]
public async Task GetTest()
{
    HttpContext.Response.ContentType = "text/plain";
    using (var writer = new StreamWriter(HttpContext.Response.Body))
        await writer.WriteLineAsync("Hello World");            
}

Firefox / Edge браузеры показывают

Привет, мир

пока Chrome/Postman сообщают об ошибке:

Страница localhost не работает

localhost неожиданно закрыл соединение.

ERR_INCOMPLETE_CHUNKED_ENCODING

PS Я собираюсь передавать много контента, поэтому не могу заранее указать заголовок Content-Length.

4 ответа

Решение

Для потоковой передачи ответа, который должен отображаться в браузере как загруженный файл, следует использовать FileStreamResult:

[HttpGet]
public FileStreamResult GetTest()
{
  var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World"));
  return new FileStreamResult(stream, new MediaTypeHeaderValue("text/plain"))
  {
    FileDownloadName = "test.txt"
  };
}

@Developer4993 был прав в том, что для отправки данных клиенту до того, как весь ответ будет проанализирован, необходимо Flushв поток ответов. Однако их ответ немного нетрадиционен с обоимиDELETE и Synchronized.StreamWriter. Кроме того, Asp.Net Core 3.x выдаст исключение, если ввод-вывод будет синхронным. Это проверено в Asp.Net Core 3.1:

[HttpGet]
public async Task Get()
{
    Response.ContentType = "text/plain";
    StreamWriter sw;
    await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
    {
        foreach (var item in someReader.Read())
        {
            await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
            await sw.FlushAsync().ConfigureAwait(false);
        }
    }
}

Предполагая someReader выполняет итерацию результатов базы данных или некоторого потока ввода-вывода с большим объемом контента, который вы не хотите буферизовать перед отправкой, это будет записывать фрагмент текста в поток ответа с каждым FlushAsync(). Для моих целей использование результатов сHttpClientбыла важнее совместимости с браузером, но если вы отправите достаточно текста, вы увидите, что браузер Chrome использует результаты в потоковом режиме. Сначала кажется, что браузер буферизует определенное количество.

Где это становится более полезным, так это с последними IAsyncEnumerable потоки, где ваш источник интенсивен либо по времени, либо по диску, но может быть получен постепенно:

[HttpGet]
public async Task<EmptyResult> Get()
{
    Response.ContentType = "text/plain";
    StreamWriter sw;
    await using ((sw = new StreamWriter(Response.Body)).ConfigureAwait(false))
    {
        await foreach (var item in GetAsyncEnumerable())
        {
            await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
            await sw.FlushAsync().ConfigureAwait(false);
        }
    }
    return new EmptyResult();
}

Вы можете бросить await Task.Delay(1000) в любой foreach для демонстрации непрерывного стриминга.

Наконец, @StephenCleary FileCallbackResultработает так же, как эти два примера. Просто немного страшнееFileResultExecutorBase из глубины недр Infrastructure пространство имен.

[HttpGet]
public IActionResult Get()
{
    return new FileCallbackResult(new MediaTypeHeaderValue("text/plain"), async (outputStream, _) =>
    {
        StreamWriter sw;
        await using ((sw = new StreamWriter(outputStream)).ConfigureAwait(false))
        {
            foreach (var item in someReader.Read())
            {
                await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
                await sw.FlushAsync().ConfigureAwait(false);
            }
        }
        outputStream.Close();
    });
}

Можно вернуть null или же EmptyResult() (которые эквивалентны), даже когда ранее писал Response.Body, Это может быть полезно, если метод возвращает ActionResult чтобы иметь возможность использовать все остальные результаты (например, BadQuery()) без труда.

[HttpGet("test")]
public ActionResult Test()
{
    Response.StatusCode = 200;
    Response.ContentType = "text/plain";
    using (var sw = new StreamWriter(Response.Body))
    {
        sw.Write("something");
    }
    return null;
}

Мне было интересно, как это сделать, и я обнаружил, что исходный код вопроса действительно работает нормально ASP.NET Core 2.1.0-rc1-finalНи Chrome (и немного других браузеров), ни приложение JavaScript не перестают работать с такой конечной точкой.

Незначительные вещи, которые я хотел бы добавить, это просто установить StatusCode и закрыть поток ответа, чтобы ответ был выполнен:

[HttpGet("test")]
public void Test()
{
    Response.StatusCode = 200;
    Response.ContentType = "text/plain";
    using (Response.Body)
    {
        using (var sw = new StreamWriter(Response.Body))
        {
            sw.Write("Hi there!");
        }
    }
}

Этот вопрос немного старше, но я нигде не мог найти лучшего ответа на то, что я пытался сделать. Чтобы отправить текущий буферизованный вывод клиенту, вы должны вызватьFlush()для каждого фрагмента контента, который вы хотите написать. Просто сделайте следующее:

[HttpDelete]
public void Content()
{
    Response.StatusCode = 200;
    Response.ContentType = "text/html";

    // the easiest way to implement a streaming response, is to simply flush the stream after every write.
    // If you are writing to the stream asynchronously, you will want to use a Synchronized StreamWriter.
    using (var sw = StreamWriter.Synchronized(new StreamWriter(Response.Body)))
    {
        foreach (var item in new int[] { 1, 2, 3, 4, })
        {
            Thread.Sleep(1000);
            sw.Write($"<p>Hi there {item}!</p>");
            sw.Flush();
        }
    };
}

вы можете протестировать с помощью curl, используя следующую команду: curl -NX DELETE <CONTROLLER_ROUTE>/content

Что -то вроде этого может работать:

[HttpGet]
public async Task<IActionResult> GetTest()
{
    var contentType = "text/plain";
    using (var stream = new MemoryStream(Encoding.ASCII.GetBytes("Hello World")))
    return new FileStreamResult(stream, contentType);

}
Другие вопросы по тегам