Wie

10

Mit ASP.NET-Core ein mehrteiliger HTTP-Antwort erstellen würde Ich mag ein Aktionsmethode in meinem ASP.NET-Core-Controller erstellen, die eine Multipart HTTP-Antwort mehrere Dateien enthält, zurückgibt. Ich weiß, dass die Verwendung einer ZIP-Datei der empfohlene Ansatz für Websites ist, aber ich erwäge, eine solche Anfrage für eine API zu verwenden.Wie

Die Beispiele, die ich find in den ASP.NET Core-Beispielen haben konnte, sind mit mehrteiligen HTTP-Anforderungen beim Hochladen von Dateien zu tun. In meinem Fall möchte ich Dateien herunterladen.

UPDATE

ich folgende GitHub Frage aufgeworfen haben: #4933

Antwort

6

Ich habe eine allgemeinere MultipartResult Klasse geschrieben, die von ActionResult erbt nur:

Verwendung Beispiel

[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); 
    } 
} 

Implementierung

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); 
    } 
} 
+0

In Ihrer Ausgabe erwähnen Sie, dass dies "keine gute Methode zum Herunterladen von Dateien aus einem Browser aufgrund mangelnder Unterstützung ist, aber ich schreibe eine API und habe einen Client, den ich auch kontrolliere." Und in Shaun Luttins Antwort erwähnt er speziell Firefox. Zur Klärung, für welche Szenarien wird deine Antwort funktionieren? – chris

+0

Dies funktioniert für APIs, bei denen Sie eine Reihe von Dateien zurückgeben möchten. In meinem Fall muss ich die Anzahl der HTTP-Anfragen aufgrund der hohen Latenz reduzieren. Dies funktioniert nicht für Websites, auf denen Sie mehrere Dateien herunterladen möchten, da Firefox der einzige Browser mit Unterstützung ist (ich habe es ausprobiert). Ich hoffe das hilft. –

+0

Wie erreiche ich das Gegenteil: Ich habe einen Controller, der eine "multipart/mixed" in seiner Anfrage hat. Wie lese ich die einzelnen Teile/Streams? – Shimmy

6

Von MSDN

MSDN has a document that lists a lot of the multipart subtypes. Die multipart/byteranges vom Client zum Download für das Senden mehrerer Dateien in einer HTTP-Antwort am besten geeignet scheint Anwendung. Der fettgedruckte Teil ist besonders relevant.

Der Inhaltstyp multipart/byteranges ist als Teil des HTTP-Nachrichtenprotokolls definiert. Sie enthält zwei oder mehr Teile mit jeweils eigenen Feldern für Inhaltstyp und Inhaltsbereich. Die Teile werden mithilfe eines MIME-Randparameters getrennt. Es ermöglicht die binären sowie 7-Bit- und 8-Bit-Dateien werden als mehr Teile gesendet mit den Längen der Teile in der Kopfzeile eines jeden Teils angegeben wird. Beachten Sie, dass HTTP zwar die Verwendung von MIME für HTTP-Dokumente vorsieht, HTTP jedoch nicht streng MIME-konform ist. (Hervorhebung hinzugefügt.)

Von RFC2068

RFC2068, Abschnitt 19.2 eine Beschreibung von multipart/byteranges zur Verfügung stellt. Auch hier ist der fett gedruckte Teil relevant. Jeder Bytebereich kann seinen eigenen Content-type haben und es stellt sich heraus, dass er auch einen eigenen Content-disposition haben kann.

Die multipart/byteranges Medientyp zwei oder mehr Teile, jedes mit seinem eigenen Content-Type und Content-Range Feldern. Die Teile werden mithilfe eines MIME-Randparameters getrennt. (Hervorhebung hinzugefügt.)

Die RFC auch diese technische Definition lautet:

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 

Der beste Teil des RFC ist sein Beispiel, das die ASP.NET Bohrkern unten zeigt.

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-- 

Beachten Sie, dass sie zwei PDFs senden! Das ist genau das, was du brauchst.

Eine ASP.NET-Core-Ansatz

Hier ist ein Codebeispiel, das auf Firefox funktioniert. Das heißt, Firefox lädt drei Bilddateien herunter, die wir mit Paint öffnen können. The source is on GitHub.

Firefox downloads the byte ranges.

Das Beispiel verwendet app.Run().Um das Beispiel an eine Controller-Aktion anzupassen, injizieren Sie IHttpContextAccessor in Ihren Controller und schreiben Sie in Ihre Aktionsmethode auf _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); 
    } 
} 

Hinweis: Dieser Beispielcode muss vor der Produktion refaktorisiert werden.

+0

Vielen Dank guter Herr! –

+2

Es stellt sich heraus, dass ich den Conte-Range-HTTP-Header nicht benötigt habe, er wird verwendet, wenn Sie einen bestimmten Teil einer Datei abrufen und nicht die gesamte Datei wie ich bin. –