การส่งคืนไฟล์ไบนารีจากคอนโทรลเลอร์ใน ASP.NET Web API


323

ฉันกำลังทำงานกับบริการเว็บโดยใช้ WebAPI ใหม่ของ ASP.NET MVC ที่จะให้บริการไฟล์ไบนารีส่วนใหญ่.cabและ.exeไฟล์

ดูเหมือนว่าวิธีการควบคุมต่อไปนี้จะใช้งานได้ซึ่งหมายความว่าจะคืนค่าไฟล์ แต่เป็นการตั้งค่าประเภทเนื้อหาเป็น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"));
}

มีวิธีที่ดีกว่าในการทำเช่นนี้?


2
ใครก็ตามที่ต้องการส่งอาร์เรย์ไบต์ผ่านสตรีมผ่านเว็บ api และ IHTTPActionResult จากนั้นดูที่นี่: nodogmablog.bryanhogan.net/2017/02/ …
IbrarMumtaz

คำตอบ:


516

ลองใช้แบบง่ายHttpResponseMessageโดยContentตั้งค่าคุณสมบัติเป็น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;
}

บางสิ่งที่ควรทราบเกี่ยวกับการstreamใช้:

  • คุณต้องไม่เรียกใช้stream.Dispose()เนื่องจาก Web API ยังคงต้องสามารถเข้าถึงได้เมื่อประมวลผลเมธอดคอนโทรลเลอร์resultเพื่อส่งข้อมูลกลับไปยังไคลเอ็นต์ ดังนั้นอย่าใช้using (var stream = …)บล็อก Web API จะกำจัดกระแสข้อมูลให้คุณ

  • ตรวจสอบให้แน่ใจว่ากระแสมีการตั้งค่าตำแหน่งปัจจุบันเป็น 0 (เช่นจุดเริ่มต้นของข้อมูลของกระแส) ในตัวอย่างด้านบนสิ่งนี้ได้รับเนื่องจากคุณเพิ่งเปิดไฟล์เท่านั้น อย่างไรก็ตามในสถานการณ์อื่น ๆ (เช่นเมื่อคุณเขียนข้อมูลไบนารีไปยัง a MemoryStream) เป็นครั้งแรกให้ตรวจสอบstream.Seek(0, SeekOrigin.Begin);หรือตั้งค่าstream.Position = 0;

  • ด้วยการสตรีมไฟล์การระบุFileAccess.Readสิทธิ์อย่างชัดเจนสามารถช่วยป้องกันปัญหาสิทธิ์การเข้าถึงบนเว็บเซิร์ฟเวอร์ บัญชีกลุ่มแอปพลิเคชัน IIS มักจะให้สิทธิ์การเข้าถึงแบบอ่าน / รายการ / สั่งการกับ wwwroot เท่านั้น


37
คุณจะรู้ได้อย่างไรเมื่อกระแสถูกปิด ฉันสมมติว่ากรอบท้ายที่สุดเรียก HttpResponseMessage.Dispose () ซึ่งจะเรียก HttpResponseMessage.Content.Dispose () ได้อย่างมีประสิทธิภาพ
Steve Guidi

41
Steve - คุณถูกต้องและฉันตรวจสอบโดยการเพิ่มเบรกพอยต์เพื่อ FileStream.Dispose และใช้รหัสนี้ กรอบการเรียก HttpResponseMessage.Dispose ซึ่งเรียก StreamContent.Dispose ซึ่งเรียก FileStream.Dispose
Dan Gartner

15
คุณไม่สามารถเพิ่ม a usingไปยังผลลัพธ์ ( HttpResponseMessage) หรือสตรีมเองได้เนื่องจากจะยังคงใช้นอกวิธีการ ดังที่ @Dan กล่าวถึงพวกเขาถูกกำจัดโดยกรอบงานหลังจากส่งการตอบสนองไปยังลูกค้าเรียบร้อยแล้ว
carlosfigueira

2
@ B.ClayShannon ใช่มันเกี่ยวกับมัน เท่าที่ลูกค้ามีความกังวลก็เป็นเพียงพวงของไบต์ในเนื้อหาของการตอบสนอง HTTP ไคลเอนต์สามารถทำอะไรกับไบต์เหล่านั้นสิ่งที่พวกเขาเลือกรวมถึงการบันทึกลงในไฟล์ท้องถิ่น
carlosfigueira

5
@carlosfigueira สวัสดีคุณรู้วิธีการลบไฟล์หลังจากที่ส่งไบต์ทั้งหมดหรือไม่
Zach

137

สำหรับWeb API 2คุณสามารถใช้งานIHttpActionResultได้ นี่คือของฉัน:

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

ดังนั้นสิ่งนี้ในคอนโทรลเลอร์ของคุณ:

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

และนี่เป็นวิธีหนึ่งที่คุณสามารถบอก IIS ให้เพิกเฉยต่อคำขอด้วยส่วนขยายเพื่อให้คำขอนั้นส่งไปยังตัวควบคุม:

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

1
คำตอบที่ดีไม่ใช่ว่ารหัส SO จะทำงานหลังจากวางและในบางกรณี (ไฟล์ต่างกัน)
Krzysztof Morcinek

1
@JonyAdamit ขอบคุณ ฉันคิดว่าตัวเลือกอื่นคือการasyncปรับเปลี่ยนในลายเซ็นวิธีการและลบการสร้างงานทั้งหมด: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby

4
แค่หัวขึ้นสำหรับทุกคนที่มาใช้ IIS7 + นี้ ตอนนี้สามารถละเว้น runAllManagedModulesForAllRequests ได้แล้ว
ดัชนี

1
@BendEg ดูเหมือนว่าในครั้งเดียวฉันจะตรวจสอบแหล่งที่มาและมันก็ทำ และมันทำให้รู้สึกว่าควร ไม่สามารถควบคุมแหล่งที่มาของเฟรมเวิร์กคำตอบสำหรับคำถามนี้อาจเปลี่ยนแปลงได้ตลอดเวลา
Ronnie Overby

1
จริงๆแล้วมีอยู่แล้วในระดับ FileResult (และแม้กระทั่ง FileStreamResult)
BrainSlugs83

12

สำหรับผู้ที่ใช้. NET Core:

คุณสามารถใช้ประโยชน์จากส่วนต่อประสาน IActionResult ในวิธีการควบคุม API เช่น ...

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

ตัวอย่างนี้ง่าย แต่ควรเข้าใจประเด็น ใน. NET Core กระบวนการนี้ง่ายกว่าใน. NET รุ่นก่อนหน้าอย่างมากเช่นไม่มีการตั้งค่าประเภทการตอบสนองเนื้อหาส่วนหัว ฯลฯ

แน่นอนว่าประเภท MIME สำหรับไฟล์และนามสกุลจะขึ้นอยู่กับความต้องการของแต่ละบุคคล

การอ้างอิง: SO โพสต์คำตอบโดย @NKosi


1
เพียงบันทึกถ้าเป็นภาพและคุณต้องการให้สามารถดูได้ในเบราว์เซอร์ที่มีการเข้าถึง URL โดยตรงไม่ต้องระบุชื่อไฟล์
พลูโต

9

ในขณะที่วิธีการแก้ปัญหาที่แนะนำทำงานได้ดีมีวิธีการส่งกลับอาร์เรย์แบบไบต์จากตัวควบคุมโดยมีรูปแบบการตอบสนองที่เหมาะสม:

  • ในคำขอตั้งค่าหัวข้อ "ยอมรับ: application / octet-stream"
  • ฝั่งเซิร์ฟเวอร์เพิ่มตัวจัดรูปแบบชนิดสื่อบันทึกเพื่อสนับสนุนชนิด mime นี้

น่าเสียดายที่ WebApi ไม่มีการจัดรูปแบบใด ๆ สำหรับ "application / octet-stream" มีการใช้งานที่นี่ใน GitHub: BinaryMediaTypeFormatter (มีการดัดแปลงเล็กน้อยเพื่อให้ทำงานได้กับ webapi 2, การเปลี่ยนลายเซ็นวิธี)

คุณสามารถเพิ่มตัวจัดรูปแบบนี้ในการกำหนดค่าส่วนกลางของคุณ:

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

ตอนนี้ควรใช้ WebApi BinaryMediaTypeFormatterหากคำขอระบุส่วนหัว Accept ที่ถูกต้อง

ฉันชอบโซลูชันนี้เพราะตัวควบคุมการดำเนินการที่ส่งกลับไบต์ [] สะดวกสบายในการทดสอบ แม้ว่าโซลูชันอื่นจะช่วยให้คุณสามารถควบคุมได้มากขึ้นหากคุณต้องการส่งคืนเนื้อหาประเภทอื่นนอกเหนือจาก "application / octet-stream" (ตัวอย่างเช่น "image / gif")


8

สำหรับทุกคนที่มีปัญหาเกี่ยวกับ API ที่ถูกเรียกมากกว่าหนึ่งครั้งในขณะที่ดาวน์โหลดไฟล์ขนาดใหญ่พอสมควรโดยใช้วิธีการในคำตอบที่ยอมรับโปรดตั้งบัฟเฟอร์การตอบสนองเป็นจริง System.Web.HttpContext.Current.Response.Buffer = true;

สิ่งนี้ทำให้แน่ใจว่าเนื้อหาไบนารีทั้งหมดถูกบัฟเฟอร์ที่ฝั่งเซิร์ฟเวอร์ก่อนที่จะถูกส่งไปยังไคลเอนต์ มิฉะนั้นคุณจะเห็นคำขอหลายคำขอถูกส่งไปยังตัวควบคุมและหากคุณไม่จัดการอย่างถูกต้องไฟล์จะเสียหาย


3
Bufferคุณสมบัติได้รับการคัดค้านBufferOutputในความโปรดปรานของ trueมันเริ่มต้นที่
ตัดสินใจ

6

โอเวอร์โหลดที่คุณใช้ตั้งค่าการแจงนับฟอร์แมตฟอร์แมตอนุกรม คุณต้องระบุประเภทเนื้อหาอย่างชัดเจนเช่น:

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

3
ขอบคุณสำหรับการตอบกลับ. ฉันลองสิ่งนี้แล้วและฉันยังเห็นContent Type: application/jsonใน Fiddler Content Typeดูเหมือนจะตั้งอย่างถูกต้องถ้าฉันทำลายก่อนที่จะกลับมาhttpResponseMessageตอบสนอง มีแนวคิดอื่นอีกไหม?
Josh Earl

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.