Comment renvoyer un fichier (FileContentResult) dans ASP.NET WebAPI

173

Dans un contrôleur MVC standard, nous pouvons générer un pdf avec un fichier FileContentResult.

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

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

Mais comment pouvons-nous le changer en un ApiController?

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

Voici ce que j'ai essayé mais cela ne semble pas fonctionner.

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

Le résultat renvoyé affiché dans le navigateur est:

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

Et il existe un article similaire sur SO: Renvoyer un fichier binaire à partir du contrôleur dans l'API Web ASP.NET . Il parle de la sortie d'un fichier existant. Mais je ne pouvais pas le faire fonctionner avec un flux.

Aucune suggestion?

Blaise
la source
1
Ce message m'a aidé: stackoverflow.com/a/23768883/585552
Greg

Réponses:

199

Au lieu de revenir en StreamContenttant que Content, je peux le faire fonctionner avec 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
la source
2
Si la moitié supérieure répond à votre question, veuillez ne la publier qu'en tant que réponse. La seconde moitié semble être une question différente - postez une nouvelle question pour cela.
gunr2171
3
Salut, merci pour le partage, j'ai une question simple (je suppose). J'ai un frontal C # qui reçoit le httpresponsemessage. Comment extraire le contenu du flux et le rendre disponible afin qu'un utilisateur puisse l'enregistrer sur le disque ou quelque chose (et je peux obtenir le fichier réel)? Merci!
Ronald
7
J'essayais de télécharger un fichier Excel auto-généré. L'utilisation de stream.GetBuffer () retournait toujours un fichier Excel corrompu. Si à la place j'utilise stream.ToArray (), le fichier est généré sans problème. J'espère que cela aide quelqu'un.
afnpires le
4
@AlexandrePires C'est parce MemoryStream.GetBuffer()que renvoie en fait le tampon du MemoryStream, qui est généralement plus grand que le contenu du flux (pour rendre les insertions efficaces). MemoryStream.ToArray()renvoie le tampon tronqué à la taille du contenu.
M.Stramm
19
Veuillez arrêter de faire cela. Ce genre d'abus de MemoryStream provoque un code non évolutif et ignore complètement le but des Streams. Réfléchissez: pourquoi tout n'est-il pas simplement exposé comme des byte[]tampons à la place? Vos utilisateurs peuvent facilement exécuter votre application à court de mémoire.
makhdumi
97

Si vous souhaitez revenir, IHttpActionResultvous pouvez le faire comme ceci:

[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
la source
3
Bonne mise à jour pour afficher le type de retour IHttpActionResult. Un refactor de ce code consisterait à déplacer un appel IHttpActionResult personnalisé tel que celui répertorié sur: stackoverflow.com/questions/23768596/…
Josh
Cet article montre une belle implémentation à usage unique. Dans mon cas, la méthode d'assistance répertoriée dans le lien ci-dessus s'est avérée plus utile
hanzolo
45

Cette question m'a aidé.

Alors, essayez ceci:

Code contrôleur:

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

Afficher le balisage HTML (avec événement de clic et 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
la source
1
Ici, vous utilisez FileStreampour un fichier existant sur le serveur. C'est un peu différent de MemoryStream. Mais merci pour la contribution.
Blaise
4
Si vous lisez à partir d'un fichier sur un serveur Web, veillez à utiliser la surcharge pour FileShare.Read, sinon vous risquez de rencontrer des exceptions d'utilisation de fichier.
Jeremy Bell
si vous le remplacez par un flux de mémoire, cela ne fonctionnera pas?
aleha
@JeremyBell c'est juste un exemple simplifié, personne ne parle ici de production et de version à sécurité intégrée.
aleha
1
@Blaise Voir ci-dessous pourquoi ce code fonctionne avec FileStreammais échoue avec MemoryStream. Cela a essentiellement à voir avec le Stream Position.
M.Stramm
9

Voici une implémentation qui diffuse le contenu du fichier sans le mettre en mémoire tampon (la mise en mémoire tampon dans l'octet [] / MemoryStream, etc. peut être un problème de serveur s'il s'agit d'un gros fichier).

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

Il peut être simplement utilisé comme ceci:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}
Simon Mourier
la source
Comment supprimeriez-vous le fichier une fois le téléchargement terminé? Y a-t-il des hooks à notifier lorsque le téléchargement est terminé?
costa
ok, la réponse semble être d'implémenter un attribut de filtre d'action et de supprimer le fichier dans la méthode OnActionExecuted.
costa
5
J'ai trouvé ce message La réponse de Risord: stackoverflow.com/questions/2041717/… . On peut utiliser cette ligne à la var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);place deFile.OpenRead(FilePath)
costa
7

Je ne sais pas exactement quelle partie blâmer, mais voici pourquoi MemoryStreamne fonctionne pas pour vous:

Au fur et à mesure que vous écrivez MemoryStream, il incrémente sa Positionpropriété. Le constructeur de StreamContentprend en compte le courant du flux Position. Donc, si vous écrivez dans le flux, puis passez-le à StreamContent, la réponse commencera à partir du néant à la fin du flux.

Il y a deux façons de résoudre correctement ce problème:

1) construire du contenu, écrire dans le flux

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

2) écrire dans le flux, réinitialiser la position, créer du contenu

[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) semble un peu mieux si vous avez un nouveau Stream, 1) est plus simple si votre flux ne commence pas à 0

M.Stramm
la source
Ce code ne fournit en fait aucune solution au problème, car il utilise la même approche que celle mentionnée dans la question. La question dit déjà que cela ne fonctionne pas, et je peux le confirmer. return Ok (new StreamContent (stream)) renvoie la représentation JSON de StreamContent.
Dmytro Zakharov
Mise à jour du code. Cette réponse répond en fait à la question plus subtile de «pourquoi la solution simple fonctionne-t-elle avec FileStream mais pas MemoryStream» plutôt que de savoir comment renvoyer un fichier dans WebApi.
M.Stramm
3

Pour moi, c'était la différence entre

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

et

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

Le premier renvoyait la représentation JSON de StringContent: {"Headers": [{"Key": "Content-Type", "Value": ["application / octet-stream; charset = utf-8"]}]}

Alors que le second retournait le fichier proprement dit.

Il semble que Request.CreateResponse a une surcharge qui prend une chaîne comme deuxième paramètre et cela semble avoir été ce qui a provoqué le rendu de l'objet StringContent lui-même sous forme de chaîne, au lieu du contenu réel.

EnderWiggin
la source