筆記目錄

Skip to content

ASP.NET Core Web API 檔案上傳處理方式的實踐探討

本文只記錄在後端開發工作中,針對 Web API 檔案上傳處理方式的一些思考和經驗,不涉及具體實作程式碼。由於知識有限,不會涉及分片上傳等進階作法。

開發模式與資料接收方式

  • 全端開發:在沒特別用 ajax 處理的情況下,大部分都是直接 Submit 表單資料,後端使用 [FromForm] 接收資料。
  • 前後端分離:大部分使用 ajax 傳遞 JSON,後端使用 [FromBody] 接收資料。

兩種主要處理方法

方法一:使用表單 Submit 或 FormData

優點

  • 不增加資料傳輸量。

缺點

  • ASP.NET Core Web API 在綁定複雜型別時,[FromBody][FromForm] 是使用不同的 ModelBinder 進行處理,如果要針對特定自訂型別進行處理需要同時為 [FormForm] 寫新的 ModelBinder,為 [FromBody] 寫新的 JsonConverter
  • 系統同時有兩種處理方式,會讓後續維護的開發人員不知道應該使用哪種方式,造成開發標準不一致。
  • 對前端影響較大,傳遞寫法需要整個改變,特別是當 API 原本沒有檔案上傳功能後來新增時。

方法二:使用 JSON 傳遞 Base64 字串

優點

  • 前後端處理方便,不需要進行額外處理方式。
  • API 格式一致性高。

缺點

  • 資料大小會增加約 1.33 倍,可能造成效能問題。

前後端專案師衝突案例

當然以 JSON 傳遞 Base64 對前後端都比較方便,不需要進行額外處理。之前公司有位後端工程師不想用這種方式,改成 [FromForm] 方式,希望前端使用 FormData 來傳遞,結果造成爭執。

我不能說那位工程師的作法錯誤,畢竟他可能是想用效能更好的方式實作,但在團隊已有習慣作法的情況下,應先溝通再實施。我當時沒深入了解他的具體作法,不確定他是要把檔案上傳單獨拆分成一個 API,還是將所有涉及檔案的 API 都改為 [FromForm]。若是後者,爭執就更可理解了。

對後端而言,這種變更改動較小,只需在複雜型別前加上 [FromForm] 標註。但對前端而言,意味著整個傳輸方式都要調整。

推薦方案:檔案上傳與業務資料提交分離設計

這是前幾個月有位同事分享,我覺得相對較好的作法:

  1. 建立單獨的檔案上傳 API (使用 [FromForm])。
  2. 在檔案資料表記錄檔案資訊,並回傳檔案 ID。
  3. 在主要的業務資料新增/修改 API 中傳遞檔案 ID 而非檔案內容。

優點

  • 不增加資料傳輸量。
  • 前端處理更簡單。
  • API 格式一致性高。

補充說明: 這種方式也解決了我在實作中遇到的一個問題。原本在上傳檔案時,業務資料新增的 API 中檔案資訊沒有 ID,但修改時可能需要變更特定檔案,因此前端需要額外傳遞 ID,導致新增和修改的 API 格式不同。通過此方法,兩者都只需傳遞檔案 ID,保持了一致性。

具體實作方式

根據同事的分享和我的實際經驗,我整理出以下實作細節:

  1. 檔案資料結構設計

    • 建立專門的檔案附件資料表,記錄檔名、儲存位置、檔案大小、下載次數、建立時間、是否啟用等資訊。
    • 業務資料主表記錄檔案 ID,或建立關聯表處理一對多的情況。
  2. 檔案上傳流程

    • 前端先調用檔案上傳 API,將檔案直接傳給後端。
    • 後端收到檔案後存儲到檔案伺服器,同時將檔案資訊寫入檔案資料表。
    • 後端返回檔案 ID 給前端。
    • 前端進行業務資料新增/修改時,只需傳遞檔案 ID 即可。
  3. 資料管理機制

    • 當業務資料新增時,將檔案狀態標記為「啟用」。
    • 設定排程任務,定期清理一天以上未啟用的檔案及其資料表記錄,避免佔用過多空間。
    • 刪除業務資料時,可選擇同時刪除關聯的檔案,或保留檔案但標記為「非啟用」。
  4. 檔案資料驗證

    • 建立自訂 ValidationAttribute 來驗證檔案資料的合法性。
    • 由於檔案資訊已存入資料庫,可以透過依賴注入(DI)方式在驗證特性中取得檔案資訊:
    csharp
    protected override ValidationResult IsValid(
        object value, ValidationContext validationContext
    ) {
        using IServiceScope scope = validationContext.CreateScope();
        // 取得相應的 Service 或是 Repository 或是 EfContext
        IFileService service = scope.ServiceProvider.GetRequiredService<IFileService>();
    }
    • 注意檔案類型驗證的局限性:無法檢核副檔名竄改的情況,即使使用檔案簽名識別,除了 .exe 文件外,大多數檔案類型要麼識別難度高,要麼容易產生誤判。
    • IValidatableObjectValidate 也可以用此方式注入物件,但我個人是不太喜歡在 Request Input 裡取得資料庫資料,涉及到資料庫資料的邏輯驗證,還是偏好在 Action 或 Service 處理。

延伸探討:檔案下載實作考量

在將檔案上傳獨立成一個 API 後,檔案下載的實作也面臨兩種設計選擇:

1. 通用檔案下載 API (FileController)

  • 優點:實作簡單,一個 API 適用所有場景,程式碼複用性高
  • 缺點:權限控制困難,無法針對不同業務場景設置細粒度的權限檢查

2. 在業務邏輯 Controller 中實作下載 API (OrderController.DownloadInvoice())

  • 優點:可實作精細的權限控制,業務邏輯整合性高
  • 缺點:各業務模組需撰寫類似的下載邏輯,實作複雜度較高

個人偏好

我傾向於將檔案下載功能整合到業務邏輯 Controller 中。若最終只是純粹下載,沒有使用任何額外的權限控管邏輯,頂多算是過度設計;反之,若採用通用下載方法,後續遇到需權限控管的檔案時,改動成本會更高,或需要為特定檔案建立額外處理機制。

為減少重複程式碼,可建立基礎的檔案下載服務類,封裝通用邏輯,再由各業務模組透過依賴注入使用。

異動歷程

  • 2025-03-10 初版文件建立。