Как вести потоковую передачу с 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);
}