# 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` 文件外,大多數檔案類型要麼識別難度高,要麼容易產生誤判。 * `IValidatableObject` 的 `Validate` 也可以用此方式注入物件,但我個人是不太喜歡在 Request Input 裡取得資料庫資料,涉及到資料庫資料的邏輯驗證,還是偏好在 Action 或 Service 處理。 ## 延伸探討:檔案下載實作考量 在將檔案上傳獨立成一個 API 後,檔案下載的實作也面臨兩種設計選擇: **1. 通用檔案下載 API (FileController)** * 優點:實作簡單,一個 API 適用所有場景,程式碼複用性高 * 缺點:權限控制困難,無法針對不同業務場景設置細粒度的權限檢查 **2. 在業務邏輯 Controller 中實作下載 API (OrderController.DownloadInvoice())** * 優點:可實作精細的權限控制,業務邏輯整合性高 * 缺點:各業務模組需撰寫類似的下載邏輯,實作複雜度較高 ### 個人偏好 我傾向於將檔案下載功能整合到業務邏輯 Controller 中。若最終只是純粹下載,沒有使用任何額外的權限控管邏輯,頂多算是過度設計;反之,若採用通用下載方法,後續遇到需權限控管的檔案時,改動成本會更高,或需要為特定檔案建立額外處理機制。 為減少重複程式碼,可建立基礎的檔案下載服務類,封裝通用邏輯,再由各業務模組透過依賴注入使用。 ###### tags: `.NET` `.NET Core & .NET 5+` `ASP.NET Core`