Как создать многопоточный HTTP-ответ с помощью ядра ASP.NET
Я хотел бы создать метод действия в моем базовом контроллере ASP.NET, который возвращает HTTP-ответ Multipart, содержащий несколько файлов. Я знаю, что использование .zip файла - рекомендуемый подход для веб-сайтов, но я рассматриваю возможность использования такого запроса для API.
Примерами, которые я смог найти в образцах ASP.NET Core, относятся к многостраничным HTTP-запросам при загрузке файлов. В моем случае я хочу загрузить файлы.
UPDATE
Я поднял следующую проблему GitHub: # 4933
Ответы
Ответ 1
Я написал более общий MultipartResult
класс, который наследует только ActionResult
:
Пример использования
[Route("[controller]")]
public class MultipartController : Controller
{
private readonly IHostingEnvironment hostingEnvironment;
public MultipartController(IHostingEnvironment hostingEnvironment)
{
this.hostingEnvironment = hostingEnvironment;
}
[HttpGet("")]
public IActionResult Get()
{
return new MultipartResult()
{
new MultipartContent()
{
ContentType = "text/plain",
FileName = "File.txt",
Stream = this.OpenFile("File.txt")
},
new MultipartContent()
{
ContentType = "application/json",
FileName = "File.json",
Stream = this.OpenFile("File.json")
}
};
}
private Stream OpenFile(string relativePath)
{
return System.IO.File.Open(
Path.Combine(this.hostingEnvironment.WebRootPath, relativePath),
FileMode.Open,
FileAccess.Read);
}
}
Реализация
public class MultipartContent
{
public string ContentType { get; set; }
public string FileName { get; set; }
public Stream Stream { get; set; }
}
public class MultipartResult : Collection<MultipartContent>, IActionResult
{
private readonly System.Net.Http.MultipartContent content;
public MultipartResult(string subtype = "byteranges", string boundary = null)
{
if (boundary == null)
{
this.content = new System.Net.Http.MultipartContent(subtype);
}
else
{
this.content = new System.Net.Http.MultipartContent(subtype, boundary);
}
}
public async Task ExecuteResultAsync(ActionContext context)
{
foreach (var item in this)
{
if (item.Stream != null)
{
var content = new StreamContent(item.Stream);
if (item.ContentType != null)
{
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(item.ContentType);
}
if (item.FileName != null)
{
var contentDisposition = new ContentDispositionHeaderValue("attachment");
contentDisposition.SetHttpFileName(item.FileName);
content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
content.Headers.ContentDisposition.FileName = contentDisposition.FileName;
content.Headers.ContentDisposition.FileNameStar = contentDisposition.FileNameStar;
}
this.content.Add(content);
}
}
context.HttpContext.Response.ContentLength = content.Headers.ContentLength;
context.HttpContext.Response.ContentType = content.Headers.ContentType.ToString();
await content.CopyToAsync(context.HttpContext.Response.Body);
}
}
Ответ 2
Из MSDN
В MSDN есть документ, в котором перечислены многие многотипные подтипы. multipart/byteranges
представляется наиболее подходящим для отправки нескольких файлов в HTTP Ответ для загрузки клиентским приложением. Смелая часть имеет особое значение.
Тип содержимого multipart/byteranges определяется как часть протокола HTTP-сообщений. Он включает две или более частей, каждая со своими собственными полями Content-Type и Content-Range. Детали разделяются с использованием параметра границы MIME. Он позволяет бинарно, а также 7-битные и 8-битные файлы, которые будут отправляться как несколько частей, причем длины частей указаны в заголовке каждой части. Обратите внимание, что, хотя HTTP делает условия использования MIME для HTTP-документов, HTTP не является строго MIME-совместимым. (Акцент добавлен.)
От RFC2068
RFC2068, в разделе 19.2 приведено описание multipart/byteranges
. Опять же, смелая часть имеет значение. Каждый byterange может иметь свой собственный Content-type
, и он также может иметь свой собственный Content-disposition
.
Тип multipart/byteranges включает две или несколько частей, , каждая с собственными полями Content-Type и Content-Range. Детали разделяются с использованием параметра границы MIME. (Акцент добавлен.)
RFC также предоставляет это техническое определение:
Media Type name: multipart
Media subtype name: byteranges
Required parameters: boundary
Optional parameters: none
Encoding considerations: only "7bit", "8bit", or "binary" are permitted
Security considerations: none
Лучшая часть RFC - это его пример, который показывает пример базового ядра ASP.NET.
HTTP/1.1 206 Partial content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES
--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 500-999/8000
...the first range...
--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 7000-7999/8000
...the second range
--THIS_STRING_SEPARATES--
Обратите внимание, что они отправляют два PDF файла! Это то, что вам нужно.
Один базовый подход ASP.NET
Вот пример кода, который работает в Firefox. То есть, Firefox загружает три файла изображений, которые мы можем открыть с помощью Paint. Источник находится в GitHub.
![Firefox загружает байтовые диапазоны.]()
В примере используется app.Run()
. Чтобы адаптировать образец к действию контроллера, введите IHttpContextAccessor
в свой контроллер и напишите в _httpContextAccessor.HttpContext.Response
в своем методе действий.
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
public class Startup
{
private const string CrLf = "\r\n";
private const string Boundary = "--THIS_STRING_SEPARATES";
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.Run(async context =>
{
var response = context.Response;
response.ContentType = $"multipart/byteranges; boundary={Boundary}";
// TODO Softcode the 'Content-length' header.
response.ContentLength = 13646;
var contentLength = response.ContentLength.Value;
await response.WriteAsync(Boundary + CrLf);
var blue = new FileInfo("./blue.jpg");
var red = new FileInfo("./red.jpg");
var green = new FileInfo("./green.jpg");
long start = 0;
long end = blue.Length;
await AddImage(response, blue, start, end, contentLength);
start = end + 1;
end = start + red.Length;
await AddImage(response, red, start, end, contentLength);
start = end + 1;
end = start + green.Length;
await AddImage(response, green, start, end, contentLength);
response.Body.Flush();
});
}
private async Task AddImage(HttpResponse response, FileInfo fileInfo,
long start, long end, long total)
{
var bytes = File.ReadAllBytes(fileInfo.FullName);
var file = new FileContentResult(bytes, "image/jpg");
await response
.WriteAsync($"Content-type: {file.ContentType.ToString()}" + CrLf);
await response
.WriteAsync($"Content-disposition: attachment; filename={fileInfo.Name}" + CrLf);
await response
.WriteAsync($"Content-range: bytes {start}-{end}/{total}" + CrLf);
await response.WriteAsync(CrLf);
await response.Body.WriteAsync(
file.FileContents,
offset: 0,
count: file.FileContents.Length);
await response.WriteAsync(CrLf);
await response.WriteAsync(Boundary + CrLf);
}
}
Примечание: этот пример кода требует рефакторинга до достижения производительности.