Trả lại tệp nhị phân từ bộ điều khiển trong ASP.NET Web API


323

Tôi đang làm việc trên một dịch vụ web bằng WebAPI mới của ASP.NET MVC sẽ phục vụ các tệp nhị phân, chủ yếu .cab.execác tệp.

Phương thức điều khiển sau có vẻ hoạt động, nghĩa là nó trả về một tệp, nhưng nó đặt loại nội dung thành 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"));
}

Có cách nào tốt hơn để làm điều này?


2
Bất cứ ai, rồi đến muốn trả về một mảng byte qua luồng qua api web và IHTTPActionResult sau đó xem ở đây: nodogmablog.bryanhogan.net/2017/02/...
IbrarMumtaz

Câu trả lời:


516

Hãy thử sử dụng một đơn giản HttpResponseMessagevới thuộc tính của nó Contentđược đặt thành 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;
}

Một số điều cần lưu ý về việc streamsử dụng:

  • Bạn không được gọi stream.Dispose(), vì API Web vẫn cần có thể truy cập nó khi nó xử lý phương thức của bộ điều khiển resultđể gửi dữ liệu lại cho máy khách. Do đó, không sử dụng một using (var stream = …)khối. API Web sẽ loại bỏ luồng cho bạn.

  • Đảm bảo rằng luồng có vị trí hiện tại được đặt thành 0 (tức là bắt đầu dữ liệu của luồng). Trong ví dụ trên, đây là một cái được đưa ra vì bạn chỉ mới mở tệp. Tuy nhiên, trong các trường hợp khác (chẳng hạn như khi bạn lần đầu tiên viết một số dữ liệu nhị phân vào a MemoryStream), hãy đảm bảo stream.Seek(0, SeekOrigin.Begin);hoặc đặtstream.Position = 0;

  • Với các luồng tệp, việc chỉ định rõ ràng FileAccess.Readquyền có thể giúp ngăn chặn các vấn đề về quyền truy cập trên các máy chủ web; Tài khoản nhóm ứng dụng IIS thường chỉ được cung cấp đọc / liệt kê / thực thi quyền truy cập vào wwwroot.


37
Bạn có biết khi nào luồng bị đóng không? Tôi giả định rằng khung cuối cùng gọi là httpResponseMessage.Dispose (), đến lượt nó gọi là httpResponseMessage.Content.Dispose () đóng luồng một cách hiệu quả.
Steve Guidi

41
Steve - bạn đã đúng và tôi đã xác minh bằng cách thêm một điểm dừng vào FileStream.Dispose và chạy mã này. Khung công tác gọi là httpResponseMessage.Dispose, gọi StreamContent.Dispose, gọi FileStream.Dispose.
Dan Gartner

15
Bạn thực sự không thể thêm một usingkết quả ( HttpResponseMessage) hoặc chính luồng đó, vì chúng vẫn sẽ được sử dụng bên ngoài phương thức. Như @Dan đã đề cập, chúng được xử lý theo khung sau khi gửi xong phản hồi cho khách hàng.
carlosfigueira

2
@ B.ClayShannon có, đó là về nó. Theo như khách hàng quan tâm thì đó chỉ là một loạt byte trong nội dung của phản hồi HTTP. Máy khách có thể thực hiện với các byte đó bất cứ thứ gì chúng chọn, bao gồm lưu nó vào tệp cục bộ.
carlosfigueira

5
@carlosfigueira, xin chào, bạn có biết cách xóa tệp sau khi tất cả các byte được gửi không?
Zach

137

Đối với API Web 2 , bạn có thể triển khai IHttpActionResult. Đây là của tôi:

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

Sau đó, một cái gì đó như thế này trong bộ điều khiển của bạn:

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

Và đây là một cách bạn có thể yêu cầu IIS bỏ qua các yêu cầu có phần mở rộng để yêu cầu sẽ thực hiện với bộ điều khiển:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>

1
Câu trả lời hay, không phải lúc nào mã SO cũng chạy ngay sau khi dán và cho các trường hợp khác nhau (các tệp khác nhau).
Krzysztof Morcinek

1
@JonyAdamit Cảm ơn. Tôi nghĩ một tùy chọn khác là đặt một công cụ asyncsửa đổi trên chữ ký phương thức và loại bỏ hoàn toàn việc tạo ra một nhiệm vụ: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby

4
Chỉ cần một người đứng đầu cho bất cứ ai đi qua IIS7 + đang chạy này. runAllManagedModulesFor ALLRequests bây giờ có thể được bỏ qua .
Chỉ số

1
@BendEg Có vẻ như tại một thời điểm tôi đã kiểm tra nguồn và nó đã làm. Và nó có ý nghĩa rằng nó nên. Không thể kiểm soát nguồn của khung, bất kỳ câu trả lời nào cho câu hỏi này có thể thay đổi theo thời gian.
Ronnie Overby

1
Thực sự đã có một lớp FileResult (và thậm chí là FileStreamResult).
BrainSlugs83

12

Đối với những người sử dụng .NET Core:

Bạn có thể sử dụng giao diện IActionResult trong phương thức bộ điều khiển API, như vậy ...

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

Ví dụ này được đơn giản hóa, nhưng sẽ nhận được điểm. Trong .NET Lõi quá trình này là quá đơn giản hơn nhiều so với các phiên bản trước của NET - tức là không có loại thiết lập phản ứng, nội dung, tiêu đề, vv

Ngoài ra, tất nhiên loại MIME cho tệp và phần mở rộng sẽ phụ thuộc vào nhu cầu cá nhân.

Tham khảo: SO Post Trả lời bởi @NKosi


1
Chỉ cần một lưu ý, nếu đó là hình ảnh và bạn muốn nó có thể xem được trong trình duyệt có quyền truy cập URL trực tiếp, thì đừng cung cấp tên tệp.
Sao Diêm Vương

9

Mặc dù giải pháp được đề xuất hoạt động tốt, có một cách khác để trả về một mảng byte từ bộ điều khiển, với luồng phản hồi được định dạng đúng:

  • Trong yêu cầu, đặt tiêu đề "Chấp nhận: application / octet-stream".
  • Phía máy chủ, thêm một trình định dạng loại phương tiện để hỗ trợ loại mime này.

Thật không may, WebApi không bao gồm bất kỳ định dạng nào cho "application / octet-stream". Có một triển khai ở đây trên GitHub: BinaryMediaTypeFormatter (có những điều chỉnh nhỏ để làm cho nó hoạt động cho webapi 2, chữ ký phương thức đã thay đổi).

Bạn có thể thêm định dạng này vào cấu hình toàn cầu của mình:

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

Bây giờ WebApi sẽ sử dụng BinaryMediaTypeFormatternếu yêu cầu chỉ định tiêu đề Chấp nhận chính xác.

Tôi thích giải pháp này vì bộ điều khiển hành động trả về byte [] thoải mái hơn để kiểm tra. Mặc dù vậy, giải pháp khác cho phép bạn kiểm soát nhiều hơn nếu bạn muốn trả về loại nội dung khác ngoài "application / octet-stream" (ví dụ: "image / gif").


8

Đối với bất kỳ ai gặp vấn đề về API được gọi nhiều lần trong khi tải xuống một tệp khá lớn bằng phương thức trong câu trả lời được chấp nhận, vui lòng đặt bộ đệm phản hồi thành true System.Web.HttpContext.Cản.Response.Buffer = true;

Điều này đảm bảo rằng toàn bộ nội dung nhị phân được đệm ở phía máy chủ trước khi nó được gửi đến máy khách. Nếu không, bạn sẽ thấy nhiều yêu cầu được gửi đến bộ điều khiển và nếu bạn không xử lý đúng cách, tệp sẽ bị hỏng.


3
Các Buffertài sản đã được phản đối ủng hộ BufferOutput. Nó mặc định là true.
quyết định

6

Quá tải mà bạn đang sử dụng sẽ thiết lập bảng liệt kê các trình định dạng tuần tự hóa. Bạn cần chỉ định loại nội dung rõ ràng như:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

3
Cảm ơn vi đa trả lơi. Tôi đã thử nó, và tôi vẫn thấy Content Type: application/jsontrong Fiddler. Có Content Typevẻ như được đặt chính xác nếu tôi ngắt trước khi trả httpResponseMessagelời. Còn ý tưởng nào nữa không?
Josh Earl

3

Bạn có thể thử

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.