Renvoyer un fichier binaire à partir du contrôleur dans l'API Web ASP.NET

323

Je travaille sur un service Web utilisant la nouvelle WebAPI d'ASP.NET MVC qui servira des fichiers binaires, principalement .cabet des .exefichiers.

La méthode de contrôleur suivante semble fonctionner, ce qui signifie qu'elle renvoie un fichier, mais elle définit le type de contenu sur application/json:

public HttpResponseMessage<Stream> Post(string version, string environment, string filetype)
{
    var path = @"C:\Temp\test.exe";
    var stream = new FileStream(path, FileMode.Open);
    return new HttpResponseMessage<Stream>(stream, new MediaTypeHeaderValue("application/octet-stream"));
}

Y a-t-il une meilleure manière de faire cela?

Josh Earl
la source
2
Toute personne qui atterrit souhaitant retourner un tableau d'octets via un flux via l'API Web et IHTTPActionResult alors voir ici: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Réponses:

516

Essayez d'utiliser un simple HttpResponseMessageavec sa Contentpropriété définie sur a StreamContent:

// using System.IO;
// using System.Net.Http;
// using System.Net.Http.Headers;

public HttpResponseMessage Post(string version, string environment,
    string filetype)
{
    var path = @"C:\Temp\test.exe";
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
    result.Content = new StreamContent(stream);
    result.Content.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    return result;
}

Quelques points à noter sur l' streamutilisation:

  • Vous ne devez pas appeler stream.Dispose(), car l'API Web doit toujours pouvoir y accéder lorsqu'elle traite les méthodes du contrôleur resultpour renvoyer des données au client. Par conséquent, n'utilisez pas de using (var stream = …)bloc. L'API Web disposera du flux pour vous.

  • Assurez-vous que la position actuelle du flux est définie sur 0 (c'est-à-dire le début des données du flux). Dans l'exemple ci-dessus, c'est une donnée puisque vous venez d'ouvrir le fichier. Cependant, dans d'autres scénarios (comme lorsque vous écrivez pour la première fois des données binaires dans a MemoryStream), assurez-vous de stream.Seek(0, SeekOrigin.Begin);ou définissezstream.Position = 0;

  • Avec les flux de fichiers, la spécification explicite des FileAccess.Readautorisations peut aider à éviter les problèmes de droits d'accès sur les serveurs Web; Les comptes de pool d'applications IIS reçoivent souvent uniquement des droits d'accès en lecture / liste / exécution sur wwwroot.

carlosfigueira
la source
37
Connaissez-vous la date de fermeture du flux? Je suppose que le framework appelle finalement HttpResponseMessage.Dispose (), qui à son tour appelle HttpResponseMessage.Content.Dispose () fermant efficacement le flux.
Steve Guidi
41
Steve - vous avez raison et j'ai vérifié en ajoutant un point d'arrêt à FileStream.Dispose et en exécutant ce code. Le framework appelle HttpResponseMessage.Dispose, qui appelle StreamContent.Dispose, qui appelle FileStream.Dispose.
Dan Gartner
15
Vous ne pouvez pas vraiment ajouter un usingau résultat ( HttpResponseMessage) ou au flux lui-même, car ils seront toujours utilisés en dehors de la méthode. Comme @Dan l'a mentionné, ils sont éliminés par le framework après l'envoi de la réponse au client.
carlosfigueira
2
@ B.ClayShannon oui, c'est à peu près tout. En ce qui concerne le client, ce n'est qu'un tas d'octets dans le contenu de la réponse HTTP. Le client peut faire avec ces octets ce qu'il choisit, y compris l'enregistrer dans un fichier local.
carlosfigueira
5
@carlosfigueira, salut, savez-vous comment supprimer le fichier après l'envoi de tous les octets?
Zach
137

Pour Web API 2 , vous pouvez implémenter IHttpActionResult. Voici la mienne:

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

class FileResult : IHttpActionResult
{
    private readonly string _filePath;
    private readonly string _contentType;

    public FileResult(string filePath, string contentType = null)
    {
        if (filePath == null) throw new ArgumentNullException("filePath");

        _filePath = filePath;
        _contentType = contentType;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StreamContent(File.OpenRead(_filePath))
        };

        var contentType = _contentType ?? MimeMapping.GetMimeMapping(Path.GetExtension(_filePath));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);

        return Task.FromResult(response);
    }
}

Ensuite, quelque chose comme ça dans votre contrôleur:

[Route("Images/{*imagePath}")]
public IHttpActionResult GetImage(string imagePath)
{
    var serverPath = Path.Combine(_rootPath, imagePath);
    var fileInfo = new FileInfo(serverPath);

    return !fileInfo.Exists
        ? (IHttpActionResult) NotFound()
        : new FileResult(fileInfo.FullName);
}

Et voici une façon de dire à IIS d'ignorer les demandes avec une extension afin que la demande parvienne au contrôleur:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
la source
1
Bonne réponse, pas toujours le code SO s'exécute juste après le collage et pour différents cas (différents fichiers).
Krzysztof Morcinek
1
@JonyAdamit Merci. Je pense qu'une autre option est de placer un asyncmodificateur sur la signature de la méthode et de supprimer complètement la création d'une tâche: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby
4
Juste un avertissement pour toute personne venant sur cette IIS7 + en cours d'exécution. runAllManagedModulesForAllRequests peut désormais être omis .
Index
1
@BendEg On dirait qu'à un moment j'ai vérifié la source et c'est ce qui s'est passé. Et il est logique que ce soit le cas. Ne pouvant contrôler la source du framework, toute réponse à cette question pourrait évoluer dans le temps.
Ronnie Overby
1
Il existe en fait déjà une classe FileResult (et même FileStreamResult) intégrée.
BrainSlugs83 du
12

Pour ceux qui utilisent .NET Core:

Vous pouvez utiliser l'interface IActionResult dans une méthode de contrôleur API, comme ceci ...

    [HttpGet("GetReportData/{year}")]
    public async Task<IActionResult> GetReportData(int year)
    {
        // Render Excel document in memory and return as Byte[]
        Byte[] file = await this._reportDao.RenderReportAsExcel(year);

        return File(file, "application/vnd.openxmlformats", "fileName.xlsx");
    }

Cet exemple est simplifié, mais devrait faire passer le message. Dans .NET Core, ce processus est tellement plus simple que dans les versions précédentes de .NET - c'est-à-dire sans définir le type de réponse, le contenu, les en-têtes, etc.

En outre, bien sûr, le type MIME pour le fichier et l'extension dépendra des besoins individuels.

Référence: SO Post Answer by @NKosi

KMJungersen
la source
1
Juste une note, si c'est une image et que vous voulez qu'elle soit visible dans un navigateur avec accès direct à l'URL, ne fournissez pas de nom de fichier.
Pluton
9

Bien que la solution suggérée fonctionne correctement, il existe un autre moyen de renvoyer un tableau d'octets à partir du contrôleur, avec un flux de réponse correctement formaté:

  • Dans la demande, définissez l'en-tête "Accept: application / octet-stream".
  • Côté serveur, ajoutez un formateur de type de support pour prendre en charge ce type MIME.

Malheureusement, WebApi n'inclut aucun formateur pour "application / octet-stream". Il y a une implémentation ici sur GitHub: BinaryMediaTypeFormatter (il y a des adaptations mineures pour le faire fonctionner pour webapi 2, les signatures de méthode ont changé).

Vous pouvez ajouter ce formateur dans votre configuration globale:

HttpConfiguration config;
// ...
config.Formatters.Add(new BinaryMediaTypeFormatter(false));

WebApi doit maintenant utiliser BinaryMediaTypeFormattersi la demande spécifie l'en-tête Accept correct.

Je préfère cette solution car un contrôleur d'action renvoyant l'octet [] est plus confortable à tester. Cependant, l'autre solution vous permet plus de contrôle si vous souhaitez renvoyer un autre type de contenu que "application / octet-stream" (par exemple "image / gif").

Eric Boumendil
la source
8

Pour toute personne ayant le problème de l'API appelée plus d'une fois lors du téléchargement d'un fichier assez volumineux en utilisant la méthode dans la réponse acceptée, veuillez définir la mise en mémoire tampon des réponses sur true System.Web.HttpContext.Current.Response.Buffer = true;

Cela garantit que tout le contenu binaire est mis en mémoire tampon côté serveur avant d'être envoyé au client. Sinon, vous verrez plusieurs demandes envoyées au contrôleur et si vous ne les gérez pas correctement, le fichier sera corrompu.

JBA
la source
3
La Bufferpropriété a été dépréciée au profit de BufferOutput. Il est par défaut true.
décède
6

La surcharge que vous utilisez définit l'énumération des formateurs de sérialisation. Vous devez spécifier explicitement le type de contenu comme:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
la source
3
Merci pour la réponse. J'ai essayé et je vois toujours Content Type: application/jsondans Fiddler. Le Content Typesemble être défini correctement si je casse avant de retourner la httpResponseMessageréponse. Avez-vous d'autres idées?
Josh Earl
3

Tu pourrais essayer

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
MickySmig
la source