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] 標註。但對前端而言,意味著整個傳輸方式都要調整。
推薦方案:檔案上傳與業務資料提交分離設計
這是前幾個月有位同事分享,我覺得相對較好的作法:
- 建立單獨的檔案上傳 API (使用
[FromForm])。 - 在檔案資料表記錄檔案資訊,並回傳檔案 ID。
- 在主要的業務資料新增/修改 API 中傳遞檔案 ID 而非檔案內容。
優點:
- 不增加資料傳輸量。
- 前端處理更簡單。
- API 格式一致性高。
補充說明: 這種方式也解決了我在實作中遇到的一個問題。原本在上傳檔案時,業務資料新增的 API 中檔案資訊沒有 ID,但修改時可能需要變更特定檔案,因此前端需要額外傳遞 ID,導致新增和修改的 API 格式不同。通過此方法,兩者都只需傳遞檔案 ID,保持了一致性。
具體實作方式
根據同事的分享和我的實際經驗,我整理出以下實作細節:
檔案資料結構設計。
- 建立專門的檔案附件資料表,記錄檔名、儲存位置、檔案大小、下載次數、建立時間、是否啟用等資訊。
- 業務資料主表記錄檔案 ID,或建立關聯表處理一對多的情況。
檔案上傳流程。
- 前端先調用檔案上傳 API,將檔案直接傳給後端。
- 後端收到檔案後存儲到檔案伺服器,同時將檔案資訊寫入檔案資料表。
- 後端返回檔案 ID 給前端。
- 前端進行業務資料新增/修改時,只需傳遞檔案 ID 即可。
資料管理機制。
- 當業務資料新增時,將檔案狀態標記為「啟用」。
- 設定排程任務,定期清理一天以上未啟用的檔案及其資料表記錄,避免佔用過多空間。
- 刪除業務資料時,可選擇同時刪除關聯的檔案,或保留檔案但標記為「非啟用」。
檔案資料驗證。
- 建立自訂 ValidationAttribute 來驗證檔案資料的合法性。
- 由於檔案資訊已存入資料庫,可以透過依賴注入(DI)方式在驗證特性中取得檔案資訊:
csharpprotected override ValidationResult IsValid( object value, ValidationContext validationContext ) { using IServiceScope scope = validationContext.CreateScope(); // 取得相應的 Service 或是 Repository 或是 EfContext IFileService service = scope.ServiceProvider.GetRequiredService<IFileService>(); }- 注意檔案類型驗證的局限性:無法檢核副檔名竄改的情況,即使使用檔案簽名識別,除了
.exe文件外,大多數檔案類型要麼識別難度高,要麼容易產生誤判。 IValidatableObject的Validate也可以用此方式注入物件,但我個人是不太喜歡在 Request Input 裡取得資料庫資料,涉及到資料庫資料的邏輯驗證,還是偏好在 Action 或 Service 處理。
延伸探討:檔案下載實作考量
在將檔案上傳獨立成一個 API 後,檔案下載的實作也面臨兩種設計選擇:
1. 通用檔案下載 API (FileController):
- 優點:實作簡單,一個 API 適用所有場景,程式碼複用性高
- 缺點:權限控制困難,無法針對不同業務場景設置細粒度的權限檢查
2. 在業務邏輯 Controller 中實作下載 API (OrderController.DownloadInvoice()):
- 優點:可實作精細的權限控制,業務邏輯整合性高
- 缺點:各業務模組需撰寫類似的下載邏輯,實作複雜度較高
個人偏好
我傾向於將檔案下載功能整合到業務邏輯 Controller 中。若最終只是純粹下載,沒有使用任何額外的權限控管邏輯,頂多算是過度設計;反之,若採用通用下載方法,後續遇到需權限控管的檔案時,改動成本會更高,或需要為特定檔案建立額外處理機制。
為減少重複程式碼,可建立基礎的檔案下載服務類,封裝通用邏輯,再由各業務模組透過依賴注入使用。
異動歷程
- 2025-03-10 初版文件建立。
