ASP.NET CORE WEBAPI文件下載

  • 2020 年 4 月 11 日
  • 筆記

最近要使用ASP.NET CORE WEBAPI用來下載文件,使用的.NET CORE 3.1。考慮如下場景:

  1. 文件是程式生成的。
  2. 文件應該能兼容各種格式。
  3. 瀏覽器可以感知進行下載。

準備

經過簡單的調研,得到以下結論。

  • ASP.NET CORE 提供FileResult這種類型的ActionResult,可以直接返迴文件結果,不需要直接處理HttpResponse。
  • 通過Stream可以直接返迴文件流供瀏覽器下載。
  • FileStreamResult是FileResult的具體實現,返回值應該是此類對象。
  • Stream有多種類型,適合直接記憶體中生成文件對象的是MemoryStream。
    對目標有了基礎的了解,就可以開始動手實現了。

實現

建立好ASP.NET CORE WEBAPI工程,把生成文件的程式碼獨立出來一個函數。我這裡需要是下載一個CSV格式的文件,因此生成一個CSV文件。
對於磁碟上的文件,可以使用FileStream對象,由於我這裡需要運行中生成這個文件,需要使用MemoryStream。

using var stream = new MemoryStream();  using var writer = new StreamWriter(stream);  //生成標題  var propCollection = ttype.GetProperties();  foreach (var n in propCollection)  {      writer.Write(n.Name);      writer.Write(",");  }  writer.WriteLine();  //生成內容  foreach (var item in res)  {      foreach (var n in propCollection)      {          writer.Write(Convert.ToString(n.GetValue(item)));          writer.Write(",");      }      writer.WriteLine();  }  
  1. 請不要考慮裡面反射的相關內容,按照自己的邏輯生成CSV即可,我只是懶得改程式碼而已。
  2. 程式碼中使用到了一些新的語法特性,請注意對低版本的.NET不一定適用。
    直接返回Stream對象給Controller處理,處理程式碼如下:
var res = await info.GetAllQueryResult();  var actionresult = new FileStreamResult(res, new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("text/csv"));  return actionresult;  

CSV的Content-Type是text/csv,如果下載別的文件,請自行查詢MIME格式。

調試

直接執行上面的程式碼,直接報錯「無法讀取已經關閉的流」。猜測是離開using語句塊的時候,stream自動被關閉了。改動很簡單,去掉using語句,不再報相同錯誤。

但是返回的文件長度一直是0,單步調試發現Writer執行完畢之後,stream返回的長度是0,內容實際上並沒有寫入,想起有一個Flush(),可以添加以確保數據寫入。

單步顯示stream長度有了,但是返回的長度還是0。繼續單步調試發現Stream的Postion是停在文件結尾的,這個和直接開始讀取文件完全不一樣,文件讀取一般是從開頭開始的,於是直接設置Postion為0,問題解決。

下載能夠成功了,但是文件名一直顯示的是隨機生成的,體驗很差。設置一下FileDownloadName即可。

核心程式碼如下:

public async Task<Stream> GetAllQueryResult()  {      var stream = new MemoryStream();      var writer = new StreamWriter(stream);      //生成標題      var propCollection = ttype.GetProperties();      foreach (var n in propCollection)      {          writer.Write(n.Name);          writer.Write(",");      }      writer.WriteLine();      //生成內容      foreach (var item in res)      {          foreach (var n in propCollection)          {              writer.Write(Convert.ToString(n.GetValue(item)));              writer.Write(",");          }          writer.WriteLine();      }      writer.Flush();      stream.Position = 0;      return stream;  }  
[HttpPost("file")]  [ProducesResponseType(typeof(FileResult), Status200OK)]  public async Task<FileResult> Download()  {      var info = new Info();      var res = await info.GetAllQueryResult();        var actionresult = new FileStreamResult(res, new Microsoft.Net.Http.Headers.MediaTypeHeaderValue("text/csv"));      actionresult.FileDownloadName = "Carinfos.csv";      //Response.ContentLength = res.Length;      return actionresult;  }  

使用swagger調用,最後效果:

總結

後來查了一些資料,總結了一下:

  • MemoryStream如果使用using語句,會在離開程式碼塊的時候自動關閉,實際上ASP.NET CORE會自動處理關閉的事項,不需要使用using語句。
  • 由於生成文件的過程是從文件流的開頭一直進行到末尾的,因此向請求端返回結果時,應當重置Stream的游標,從0開始傳輸。
  • 記得在使用writer之後使用Flush()以確保數據有寫入。
  • 如果不確定文件格式,可以直接返回MIME值為application/oct-stream。
  • 設置FileStreamResult的FileDownloadName屬性可以修改文件的默認名稱。
  • (可選)可以通過設置Response.ContentLength來設置文件的長度。

參考資料:

https://darchuk.net/2019/05/31/asp-net-core-web-api-returning-a-filestream/