Cómo devolver un archivo (FileContentResult) en ASP.NET WebAPI

173

En un controlador MVC normal, podemos generar pdf con a FileContentResult.

public FileContentResult Test(TestViewModel vm)
{
    var stream = new MemoryStream();
    //... add content to the stream.

    return File(stream.GetBuffer(), "application/pdf", "test.pdf");
}

Pero, ¿cómo podemos convertirlo en un ApiController?

[HttpPost]
public IHttpActionResult Test(TestViewModel vm)
{
     //...
     return Ok(pdfOutput);
}

Esto es lo que he intentado pero no parece funcionar.

[HttpGet]
public IHttpActionResult Test()
{
    var stream = new MemoryStream();
    //...
    var content = new StreamContent(stream);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    content.Headers.ContentLength = stream.GetBuffer().Length;
    return Ok(content);            
}

El resultado devuelto que se muestra en el navegador es:

{"Headers":[{"Key":"Content-Type","Value":["application/pdf"]},{"Key":"Content-Length","Value":["152844"]}]}

Y hay una publicación similar en SO: Devolver archivo binario del controlador en ASP.NET Web API . Habla sobre la salida de un archivo existente. Pero no pude hacerlo funcionar con una transmisión.

¿Alguna sugerencia?

Blaise
fuente
1
Esta publicación me ayudó: stackoverflow.com/a/23768883/585552
Greg

Respuestas:

199

En lugar de regresar StreamContentcomo el Content, puedo hacer que funcione ByteArrayContent.

[HttpGet]
public HttpResponseMessage Generate()
{
    var stream = new MemoryStream();
    // processing the stream.

    var result = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new ByteArrayContent(stream.ToArray())
    };
    result.Content.Headers.ContentDisposition =
        new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
    {
        FileName = "CertificationCard.pdf"
    };
    result.Content.Headers.ContentType =
        new MediaTypeHeaderValue("application/octet-stream");

    return result;
}
Blaise
fuente
2
Si la mitad superior responde a su pregunta, publíquela solo como respuesta. La segunda mitad parece ser una pregunta diferente: publique una nueva pregunta para eso.
gunr2171
3
Hola, gracias por compartir, tengo una pregunta simple (supongo). Tengo una interfaz C # que recibe el mensaje httpresponse. ¿Cómo extraigo el contenido continuo y lo hago disponible para que un usuario pueda guardarlo en el disco o algo así (y puedo obtener el archivo real)? ¡Gracias!
Ronald
77
Estaba tratando de descargar un archivo de Excel autogenerado. Usando stream.GetBuffer () siempre devolvió un excel corrupto. Si, en cambio, uso stream.ToArray (), el archivo se genera sin problemas. Espero que esto ayude a alguien.
ocurre el
44
@AlexandrePires Esto se debe a que en MemoryStream.GetBuffer()realidad devuelve el búfer de MemoryStream, que generalmente es más grande que el contenido de la secuencia (para que las inserciones sean eficientes). MemoryStream.ToArray()devuelve el búfer truncado al tamaño del contenido.
M.Stramm
19
Por favor deja de hacer esto. Este tipo de abuso de MemoryStream causa un código no escalable e ignora por completo el propósito de Streams. Piensa: ¿por qué no todo está expuesto como byte[]búfer? Sus usuarios pueden ejecutar fácilmente su aplicación sin memoria.
makhdumi
97

Si quieres regresar IHttpActionResultpuedes hacerlo así:

[HttpGet]
public IHttpActionResult Test()
{
    var stream = new MemoryStream();

    var result = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new ByteArrayContent(stream.GetBuffer())
    };
    result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
    {
        FileName = "test.pdf"
    };
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

    var response = ResponseMessage(result);

    return response;
}
Ogglas
fuente
3
Buena actualización para mostrar el tipo de retorno IHttpActionResult. Un refactor de este código sería mover una llamada a un IHttpActionResult personalizado, como el que figura en: stackoverflow.com/questions/23768596/…
Josh
Esta publicación muestra una buena implementación ordenada de un solo uso. En mi caso, el método de ayuda enumerado en el enlace anterior resultó más útil
hanzolo
45

Esta pregunta me ayudó.

Entonces, prueba esto:

Código del controlador:

[HttpGet]
public HttpResponseMessage Test()
{
    var path = System.Web.HttpContext.Current.Server.MapPath("~/Content/test.docx");;
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    var stream = new FileStream(path, FileMode.Open);
    result.Content = new StreamContent(stream);
    result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
    result.Content.Headers.ContentDisposition.FileName = Path.GetFileName(path);
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
    result.Content.Headers.ContentLength = stream.Length;
    return result;          
}

Ver marcado HTML (con evento de clic y url simple):

<script type="text/javascript">
    $(document).ready(function () {
        $("#btn").click(function () {
            // httproute = "" - using this to construct proper web api links.
            window.location.href = "@Url.Action("GetFile", "Data", new { httproute = "" })";
        });
    });
</script>


<button id="btn">
    Button text
</button>

<a href=" @Url.Action("GetFile", "Data", new { httproute = "" }) ">Data</a>
aleha
fuente
1
Aquí está utilizando FileStreamun archivo existente en el servidor. Es un poco diferente de MemoryStream. Pero gracias por el aporte.
Blaise
44
Si lee de un archivo en un servidor web, asegúrese de usar la sobrecarga para FileShare.Leer, de lo contrario, puede encontrar excepciones de archivos en uso.
Jeremy Bell
si lo reemplaza con flujo de memoria no funcionará?
aleha
@JeremyBell es solo un ejemplo simplificado, nadie habla aquí sobre producción y versión a prueba de fallas.
aleha
1
@Blaise Vea a continuación por qué este código funciona FileStreampero falla MemoryStream. Básicamente tiene que ver con el Stream Position.
M.Stramm
9

Aquí hay una implementación que transmite el contenido del archivo sin almacenarlo en el búfer (el almacenamiento en búfer en el byte [] / MemoryStream, etc. puede ser un problema del servidor si es un archivo grande).

public class FileResult : IHttpActionResult
{
    public FileResult(string filePath)
    {
        if (filePath == null)
            throw new ArgumentNullException(nameof(filePath));

        FilePath = filePath;
    }

    public string FilePath { get; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = new StreamContent(File.OpenRead(FilePath));
        var contentType = MimeMapping.GetMimeMapping(Path.GetExtension(FilePath));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
        return Task.FromResult(response);
    }
}

Se puede usar simplemente así:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}
Simon Mourier
fuente
¿Cómo eliminaría el archivo después de que se haya realizado la descarga? ¿Hay algún gancho para ser notificado cuando finalice la descarga?
costa
ok, la respuesta parece ser implementar un atributo de filtro de acción y eliminar el archivo en el método OnActionExecuted.
costa
55
Encontré esta publicación Respuesta de Risord: stackoverflow.com/questions/2041717/… . Uno puede usar esta línea en var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);lugar deFile.OpenRead(FilePath)
costa
7

No estoy exactamente seguro de qué parte culpar, pero he aquí por qué MemoryStreamno funciona para usted:

A medida que escribe MemoryStream, aumenta su Positionpropiedad. El constructor de StreamContenttiene en cuenta la corriente de la corriente Position. Entonces, si escribe en la secuencia, páselo aStreamContent , la respuesta comenzará desde la nada al final de la secuencia.

Hay dos formas de arreglar esto correctamente:

1) construir contenido, escribir para transmitir

[HttpGet]
public HttpResponseMessage Test()
{
    var stream = new MemoryStream();
    var response = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(stream);
    // ...
    // stream.Write(...);
    // ...
    return response;
}

2) escribir para transmitir, restablecer posición, construir contenido

[HttpGet]
public HttpResponseMessage Test()
{
    var stream = new MemoryStream();
    // ...
    // stream.Write(...);
    // ...
    stream.Position = 0;

    var response = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(stream);
    return response;
}

2) se ve un poco mejor si tiene una transmisión nueva, 1) es más simple si su transmisión no comienza en 0

M.Stramm
fuente
Este código en realidad no proporciona ninguna solución al problema, ya que utiliza el mismo enfoque que se mencionó en la pregunta. La pregunta ya dice que esto no funciona, y puedo confirmarlo. return Ok (nuevo StreamContent (stream)) devuelve la representación JSON de StreamContent.
Dmytro Zakharov
Se actualizó el código. Esta respuesta en realidad responde a la pregunta más sutil de 'por qué la solución simple funciona con FileStream pero no con MemoryStream' en lugar de cómo devolver un archivo en WebApi.
M.Stramm
3

Para mí fue la diferencia entre

var response = Request.CreateResponse(HttpStatusCode.OK, new StringContent(log, System.Text.Encoding.UTF8, "application/octet-stream");

y

var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(log, System.Text.Encoding.UTF8, "application/octet-stream");

El primero fue devolver la representación JSON de StringContent: {"Headers": [{"Key": "Content-Type", "Value": ["application / octet-stream; charset = utf-8"]}]}

Mientras que el segundo devolvía el archivo correctamente.

Parece que Request.CreateResponse tiene una sobrecarga que toma una cadena como el segundo parámetro y esto parece haber sido lo que estaba causando que el objeto StringContent se representara como una cadena, en lugar del contenido real.

EnderWiggin
fuente