Devolución de archivo binario desde el controlador en ASP.NET Web API

323

Estoy trabajando en un servicio web utilizando el nuevo WebAPI de ASP.NET MVC que servirá archivos binarios, principalmente .caby .exearchivos.

El siguiente método de controlador parece funcionar, lo que significa que devuelve un archivo, pero establece el tipo de contenido en 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"));
}

¿Hay una mejor manera de hacer esto?

Josh Earl
fuente
2
Cualquiera que aterrice que desee devolver una matriz de bytes a través de la transmisión a través de la API web y IHTTPActionResult, consulte aquí: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz el

Respuestas:

516

Intente usar un simple HttpResponseMessagecon su Contentpropiedad establecida en 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;
}

Algunas cosas a tener en cuenta sobre el streamuso:

  • No debe llamar stream.Dispose(), ya que la API web aún necesita poder acceder a ella cuando procesa los métodos del controlador resultpara enviar datos al cliente. Por lo tanto, no use un using (var stream = …)bloque. La API web eliminará la transmisión por usted.

  • Asegúrese de que la secuencia tenga su posición actual establecida en 0 (es decir, el comienzo de los datos de la secuencia). En el ejemplo anterior, esto es un hecho dado que acaba de abrir el archivo. Sin embargo, en otros escenarios (como cuando escribe por primera vez algunos datos binarios en a MemoryStream), asegúrese de stream.Seek(0, SeekOrigin.Begin);establecerstream.Position = 0;

  • Con las secuencias de archivos, la especificación explícita de FileAccess.Readpermisos puede ayudar a prevenir problemas de derechos de acceso en servidores web; Las cuentas del grupo de aplicaciones IIS a menudo solo tienen derechos de acceso de lectura / lista / ejecución para wwwroot.

carlosfigueira
fuente
37
¿Sabrías cuándo se cierra la corriente? Supongo que el marco finalmente llama a HttpResponseMessage.Dispose (), que a su vez llama a HttpResponseMessage.Content.Dispose () cerrando efectivamente la secuencia.
Steve Guidi
41
Steve: tienes razón y lo verifiqué agregando un punto de interrupción a FileStream. Elimina y ejecuta este código. El marco llama a HttpResponseMessage.Dispose, que llama a StreamContent.Dispose, que llama a FileStream.Dispose.
Dan Gartner
15
Realmente no puede agregar usinga ni al resultado ( HttpResponseMessage) ni a la secuencia en sí, ya que aún se usarán fuera del método. Como @Dan mencionó, el marco los elimina después de que haya terminado de enviar la respuesta al cliente.
carlosfigueira
2
@ B.ClayShannon sí, eso es todo. En lo que respecta al cliente, es solo un montón de bytes en el contenido de la respuesta HTTP. El cliente puede hacer con esos bytes lo que elija, incluido guardarlo en un archivo local.
carlosfigueira
55
@carlosfigueira, hola, ¿sabes cómo eliminar el archivo después de enviar todos los bytes?
Zach
137

Para Web API 2 , puede implementar IHttpActionResult. Aquí está el mío:

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

Entonces algo como esto en su controlador:

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

Y aquí hay una forma en que puede decirle a IIS que ignore las solicitudes con una extensión para que la solicitud llegue al controlador:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
fuente
1
Buena respuesta, no siempre el código SO se ejecuta justo después de pegar y para diferentes casos (diferentes archivos).
Krzysztof Morcinek
1
@JonyAdamit Gracias. Creo que otra opción es colocar un asyncmodificador en la firma del método y eliminar la creación de una tarea por completo: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby
44
Solo un aviso para cualquiera que venga con este IIS7 +. runAllManagedModulesForAllRequests ahora se puede omitir .
Índice del
1
@BendEg Parece que una vez revisé la fuente y lo hizo. Y tiene sentido que así sea. Al no poder controlar la fuente del marco, cualquier respuesta a esta pregunta podría cambiar con el tiempo.
Ronnie Overby
1
En realidad, ya hay una clase integrada en FileResult (e incluso FileStreamResult).
BrainSlugs83
12

Para aquellos que usan .NET Core:

Puede hacer uso de la interfaz IActionResult en un método de controlador API, así ...

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

Este ejemplo está simplificado, pero debería transmitir el punto. En .NET Core este proceso es tan mucho más simple que en las versiones anteriores de .NET - es decir, ningún tipo respuesta del ajuste, el contenido, los encabezados, etc.

Además, por supuesto, el tipo MIME para el archivo y la extensión dependerán de las necesidades individuales.

Referencia: SO Publicar respuesta por @NKosi

KMJungersen
fuente
1
Solo una nota, si se trata de una imagen y desea que se pueda ver en un navegador con acceso directo a URL, no proporcione un nombre de archivo.
Plutón
9

Si bien la solución sugerida funciona bien, hay otra forma de devolver una matriz de bytes desde el controlador, con el flujo de respuesta correctamente formateado:

  • En la solicitud, establezca el encabezado "Aceptar: aplicación / flujo de octetos".
  • Del lado del servidor, agregue un formateador de tipo de medio para admitir este tipo mime.

Desafortunadamente, WebApi no incluye ningún formateador para "application / octet-stream". Aquí hay una implementación en GitHub: BinaryMediaTypeFormatter (hay pequeñas adaptaciones para que funcione en webapi 2, las firmas de los métodos cambiaron).

Puede agregar este formateador a su configuración global:

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

WebApi ahora debería usar BinaryMediaTypeFormattersi la solicitud especifica el encabezado correcto de aceptación.

Prefiero esta solución porque un controlador de acción que devuelve el byte [] es más cómodo de probar. Sin embargo, la otra solución le permite un mayor control si desea devolver otro tipo de contenido que "application / octet-stream" (por ejemplo, "image / gif").

Eric Boumendil
fuente
8

Para cualquiera que tenga el problema de que se llame a la API más de una vez mientras descarga un archivo bastante grande utilizando el método en la respuesta aceptada, establezca el búfer de respuesta en verdadero System.Web.HttpContext.Current.Response.Buffer = true;

Esto asegura que todo el contenido binario se almacena en el lado del servidor antes de enviarlo al cliente. De lo contrario, verá que se envían varias solicitudes al controlador y, si no lo maneja correctamente, el archivo se dañará.

JBA
fuente
3
La Bufferpropiedad ha quedado en desuso a favor de BufferOutput. Por defecto es true.
decae el
6

La sobrecarga que está utilizando establece la enumeración de los formateadores de serialización. Debe especificar el tipo de contenido explícitamente como:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
fuente
3
Gracias por la respuesta. Probé esto y todavía lo veo Content Type: application/jsonen Fiddler. El Content Typeparece estar configurado correctamente si rompo antes de devolver la httpResponseMessagerespuesta. ¿Alguna idea más?
Josh Earl
3

Tu podrías intentar

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