¿Cómo devolver un archivo usando Web API?

98

Estoy usando ASP.NET Web API .
Quiero descargar un PDF con C # desde la API (que genera la API).

¿Puedo hacer que la API devuelva un byte[]? y para la aplicación C # puedo hacer:

byte[] pdf = client.DownloadData("urlToAPI");? 

y

File.WriteAllBytes()?
Kyle
fuente
"la API web"? Qué quieres decir exactamente? Lea tinyurl.com/so-hints y edite su pregunta.
Jon Skeet
1
@JonSkeet: La API web es una característica nueva en la última versión de ASP.NET. Ver asp.net/whitepapers/mvc4-release-notes#_Toc317096197
Robert Harvey
1
@Robert: Gracias, la etiqueta lo aclara, aunque la referencia a "la API web ASP.NET" habría sido aún más clara. En parte, es culpa de MS por un nombre descabellado y genérico también :)
Jon Skeet
Cualquiera que aterrice y desee devolver la transmisión a través de la API web y IHTTPActionResult, consulte aquí: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Respuestas:

170

Es mejor devolver HttpResponseMessage con StreamContent dentro.

He aquí un ejemplo:

public HttpResponseMessage GetFile(string id)
{
    if (String.IsNullOrEmpty(id))
        return Request.CreateResponse(HttpStatusCode.BadRequest);

    string fileName;
    string localFilePath;
    int fileSize;

    localFilePath = getFileFromID(id, out fileName, out fileSize);

    HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
    response.Content = new StreamContent(new FileStream(localFilePath, FileMode.Open, FileAccess.Read));
    response.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
    response.Content.Headers.ContentDisposition.FileName = fileName;
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");

    return response;
}

UPD desde el comentario de patridge : si alguien más llega aquí buscando enviar una respuesta desde una matriz de bytes en lugar de un archivo real, querrá usar el nuevo ByteArrayContent (someData) en lugar de StreamContent (ver aquí ).

Regfor
fuente
1
Lo primero: este código provocará una excepción, ya que está renovando dos objetos FileStream que apuntan al mismo archivo. En segundo lugar, no desea utilizar una instrucción "Using", porque tan pronto como la variable salga del alcance, .NET la eliminará y obtendrá mensajes de error sobre el cierre de la conexión subyacente.
Brandon Montgomery
48
Si alguien más llega aquí buscando enviar una respuesta desde una matriz de bytes en lugar de un archivo real, querrá usar en new ByteArrayContent(someData)lugar de StreamContent(ver aquí ).
patridge
Es posible que también desee anular el dispose () base para que pueda manejar sus recursos correctamente cuando el marco lo llame.
Phil Cooper
1
Me gustaría señalar que el MediaTypeHeaderValue correcto es crucial y para que sea dinámico si tiene diferentes tipos de archivos, puede hacer esto. (donde fileName es una cadena y tiene un tipo de archivo que termina como .jpg, .pdf, docx, etc.) var contentType = MimeMapping.GetMimeMapping (fileName); response.Content.Headers.ContentType = new MediaTypeHeaderValue (contentType);
JimiSweden
1
¿FileStream se elimina automáticamente?
Brian Tacker
37

Hice la siguiente acción:

[HttpGet]
[Route("api/DownloadPdfFile/{id}")]
public HttpResponseMessage DownloadPdfFile(long id)
{
    HttpResponseMessage result = null;
    try
    {
        SQL.File file = db.Files.Where(b => b.ID == id).SingleOrDefault();

        if (file == null)
        {
            result = Request.CreateResponse(HttpStatusCode.Gone);
        }
        else
        {
            // sendo file to client
            byte[] bytes = Convert.FromBase64String(file.pdfBase64);


            result = Request.CreateResponse(HttpStatusCode.OK);
            result.Content = new ByteArrayContent(bytes);
            result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
            result.Content.Headers.ContentDisposition.FileName = file.name + ".pdf";
        }

        return result;
    }
    catch (Exception ex)
    {
        return Request.CreateResponse(HttpStatusCode.Gone);
    }
}
André de Mattos Ferraz
fuente
Esto realmente responde a la pregunta
Mick
1
Esto no sería una buena idea con archivos grandes, ya que carga la imagen completa en la memoria. La opción de transmisión es mejor.
Paul Reedy
@PaulReedy Perfecto ... pero en muchos casos, no necesitas lidiar con archivos grandes. ¡Pero estoy totalmente de acuerdo con tu punto!
André de Mattos Ferraz
14

Ejemplo con IHttpActionResulten ApiController.

[HttpGet]
[Route("file/{id}/")]
public IHttpActionResult GetFileForCustomer(int id)
{
    if (id == 0)
      return BadRequest();

    var file = GetFile(id);

    IHttpActionResult response;
    HttpResponseMessage responseMsg = new HttpResponseMessage(HttpStatusCode.OK);
    responseMsg.Content = new ByteArrayContent(file.SomeData);
    responseMsg.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
    responseMsg.Content.Headers.ContentDisposition.FileName = file.FileName;
    responseMsg.Content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    response = ResponseMessage(responseMsg);
    return response;
}

Si no desea descargar el PDF y utilizar un navegador integrado en el visor de PDF, elimine las siguientes dos líneas:

responseMsg.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
responseMsg.Content.Headers.ContentDisposition.FileName = file.FileName;
Ogglas
fuente
@ElbertJohnFelipe Sí, obtienes el archivo con var file = GetFile(id);. file.SomeDataes un Byte Array ( byte[]) y file.FileNamees string.
Ogglas
Gracias por tu publicación. 'HttpResponseMessage' no funcionó para mí dentro de un ApiController, así que me salvaste.
Max
13

Solo una nota para .Net Core: Podemos usar FileContentResulty establecer contentType en application/octet-streamsi queremos enviar los bytes sin procesar. Ejemplo:

[HttpGet("{id}")]
public IActionResult GetDocumentBytes(int id)
{
    byte[] byteArray = GetDocumentByteArray(id);
    return new FileContentResult(byteArray, "application/octet-stream");
}
Amir Shirazi
fuente
1
Esto funciona muy bien, también si quieres controlar el nombre del archivo no es una propiedad de FileContentResultllamada FileDownloadNamepara especificar el nombre del archivo
weeksdev
@weeksdev ah no lo sabía. Gracias por el comentario.
Amir Shirazi
Eso es todo, gracias. También el comentario de weeksdev es muy útil.
fragg
1

Me preguntaba si había una forma sencilla de descargar un archivo de una forma más ... "genérica". Se me ocurrió esto.

Es un simple ActionResultque le permitirá descargar un archivo de una llamada de controlador que devuelve un IHttpActionResult. El archivo se almacena en formato byte[] Content. Puede convertirlo en una secuencia si es necesario.

Usé esto para devolver archivos almacenados en la columna varbinary de una base de datos.

    public class FileHttpActionResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string FileName { get; set; }
        public string MediaType { get; set; }
        public HttpStatusCode StatusCode { get; set; }

        public byte[] Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = new HttpResponseMessage(StatusCode);

            response.StatusCode = StatusCode;
            response.Content = new StreamContent(new MemoryStream(Content));
            response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
            response.Content.Headers.ContentDisposition.FileName = FileName;
            response.Content.Headers.ContentType = new MediaTypeHeaderValue(MediaType);

            return Task.FromResult(response);
        }
    }
Jake
fuente
Una breve explicación de cómo su código soluciona el (los) problema (s) del OP mejoraría la calidad de su respuesta.
Adrian Mole