﻿# 認識 ASP․NET Core 檔案提供者與透過 Web API 下載實體檔案

昨天有位學員問到如何透過 ASP․NET Core Web API 下載檔案，原本認為這只是個簡單的問題，畢竟就只是抓個檔案嘛，哪有什麼難的，隨便 Google 兩下就有答案了。但是這個簡單的問題，卻激起我的好奇心，為什麼以往在 ASP․NET MVC 5 非常簡單的寫法，竟然到了 ASP․NET Core 就不能用了？欲知詳情，請繼續看下去！

### 錯誤的代碼

如果想要透過一個 Action 下載檔案，在 ASP․NET MVC 5 的年代，我們會這樣寫：

```csharp
public ActionResult GetFile()
{
    var filePath = Server.MapPath("~/IMG_0215.jpg");
    return File(filePath, "image/jpeg");
}
```

換到 ASP․NET Core 之後，由於並沒有 `Server` 靜態類別可用，所以很直覺的，你可能會直接先測試看看寫死「絕對路徑」嘗試看看：

```csharp
public IActionResult GetFile()
{
    return File("G:\\IMG_0215.jpg", "image/jpeg");
}
```

雖然可以成功建置，但是執行時卻會得到以下結果：

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/8e94b7eeedcaa401aea9-70392491-98e7f900-1a1b-11ea-811c-d2c3fe3c8426.png)

```
InvalidOperationException: No file provider has been configured to process the supplied file.

Microsoft.AspNetCore.Mvc.Infrastructure.VirtualFileResultExecutor.GetFileInformation(VirtualFileResult result)
```

很明顯的，這是一份可以建置，但卻無法執行的代碼！

### 循線追查原凶

將錯誤訊息到 Google/Stackoverflow 搜尋，立刻就可以找到各種建議，每一種建議都可以解決無法下載檔案的問題，但老實說，是我每個答案都不滿意，因為感覺不對！

我們就來看看網路上查到的各種答案：

1.  由於 [ControllerBase.File](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file?view=aspnetcore-3.0&WT.mc_id=DT-MVP-4015686) 總共有 24 個多型，[有人](https://stackoverflow.com/a/53633641/910074)就建議，當 [File(String, String)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file?view=aspnetcore-3.0&WT.mc_id=DT-MVP-4015686#Microsoft_AspNetCore_Mvc_ControllerBase_File_System_String_System_String_) 不能用的時候，換一個不就好了！
    
    ```csharp
    public IActionResult GetFile()
    {
        IFileProvider provider = new PhysicalFileProvider("G:\\");
        IFileInfo fileInfo = provider.GetFileInfo("IMG_0215.jpg");
        return File(fileInfo.CreateReadStream(), "image/jpeg");
    }
    ```
    
    說實在的，原本 1 行就能寫完的代碼，現在要我寫 3 行，這感覺太怪了，不合理啊！
    
    我後來找到，這段程式碼的建議，完全可以改寫成以下：
    
    ```csharp
    public IActionResult GetFile()
    {
        return PhysicalFile("G:\\IMG_0215.jpg", "image/jpeg");
    }
    ```
    
2.  接著有另外一位老兄，提出了一個[更加複雜的建議](https://stackoverflow.com/a/53631277/910074)，雖然也真的能解決問題，但就非常不討喜，完全不可取的寫法！
    
    他提到了關於 [File Providers](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?WT.mc_id=DT-MVP-4015686) 的觀念，要你先到 `Startup.ConfigureServices` 註冊一個 [IFileProvider](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.fileproviders.ifileprovider?WT.mc_id=DT-MVP-4015686) 到 DI 容器中：
    
    ```csharp
    services.AddSingleton<IFileProvider>(new PhysicalFileProvider(Directory.GetCurrentDirectory()));
    ```
    
    然後到**控制器類別的建構式**注入 [IFileProvider](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.fileproviders.ifileprovider?WT.mc_id=DT-MVP-4015686) 物件：
    
    ```csharp
    private readonly IFileProvider fileProvider;
    
    public CoursesController(IFileProvider fileProvider)
    {
        this.fileProvider = fileProvider;
    }
    ```
    
    接著先將圖片複製到專案根目錄下，然後透過 [IFileProvider.GetFileInfo(String)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.fileproviders.ifileprovider.getfileinfo?view=dotnet-plat-ext-3.0&WT.mc_id=DT-MVP-4015686#Microsoft_Extensions_FileProviders_IFileProvider_GetFileInfo_System_String_) 方法，取得 [IFileInfo](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.fileproviders.ifileinfo?view=dotnet-plat-ext-3.0&WT.mc_id=DT-MVP-4015686) 物件，並執行 [IFileInfo.CreateReadStream](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.extensions.fileproviders.ifileinfo.createreadstream?view=dotnet-plat-ext-3.0&WT.mc_id=DT-MVP-4015686#Microsoft_Extensions_FileProviders_IFileInfo_CreateReadStream) 方法，最後傳入 [File(Stream, String)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file?view=aspnetcore-3.0&WT.mc_id=DT-MVP-4015686#Microsoft_AspNetCore_Mvc_ControllerBase_File_System_IO_Stream_System_String_) 即可：
    
    ```csharp
    public IActionResult GetFile()
    {
        IFileInfo fileInfo = fileProvider.GetFileInfo("IMG_0215.jpg");
        return File(fileInfo.CreateReadStream(), "image/jpeg");
    }
    ```
    
    我的天啊！這是人寫的 Code 嗎？還我簡單且無負擔的程式碼！
    

### 研究 virtualPath 的核心原理

你從 [File(String, String)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file?view=aspnetcore-3.0&WT.mc_id=DT-MVP-4015686#Microsoft_AspNetCore_Mvc_ControllerBase_File_System_String_System_String_) 文件可以看到，他的第一個參數是 `virtualPath`，文件說明是 `The virtual path of the file to be returned.`。

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/015e9b59609d5cc7da35-70392929-c59e0f80-1a1f-11ea-9b57-829759f00ec1.png)

嗯！有看沒有懂！而且相關資料、範例都非常難找到！

接著，我認真的把 [File Providers in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-3.1&WT.mc_id=DT-MVP-4015686) 文件**看完**、**看懂**後，我發現還是完全無法理解 **File Providers** 與 **virtualPath** 之間的關係。

> 當你在無法理解一個**抽象概念**的時候，你可能連想搜尋的關鍵字都不知道該怎麼輸入！

後來我真的氣到了，決定直接去翻查 ASP.NET Core 的[原始碼](https://github.com/aspnet/AspNetCore)，直接從 [ControllerBase](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1418-L1419) 類別的 [File(string virtualPath, string contentType)](https://github.com/aspnet/AspNetCore/blob/release/3.1/src/Mvc/Mvc.Core/src/ControllerBase.cs#L1418-L1419) 開始追查起。然後循線找到 [VirtualFileResult](https://github.com/aspnet/AspNetCore/blob/7e10a98c4f303b9af36047a0c61f1fbf038ccda8/src/Mvc/Mvc.Core/src/VirtualFileResult.cs) 類別。接著再找到 [VirtualFileResultExecutor](https://github.com/aspnet/AspNetCore/blob/d7bfbb5824b5f8876bcd4afaa29a611efc7aa1c9/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs) 類別的 [ExecuteAsync](https://github.com/aspnet/AspNetCore/blob/d7bfbb5824b5f8876bcd4afaa29a611efc7aa1c9/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs#L34-L70) 方法。而這裡，正是可以讓我看出發生問題的主因！

畢竟這個多型，是傳入所謂的 `virtualPath` 進去，這是一個字串，然後執行到這裡，他最終還是要取得檔案資訊 (`IFileInfo`)。接著我就發現到，他有一段 [GetFileProvider](https://github.com/aspnet/AspNetCore/blob/d7bfbb5824b5f8876bcd4afaa29a611efc7aa1c9/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs#L126-L135) 方法，用來取得 `virtualPath` 的「預設提供者」在哪，這才發現到，原來 `VirtualFileResult` 的所謂的「虛擬路徑」(`virtualPath`)指的就是用這個**檔案提供者** ([File Provider](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-3.1&WT.mc_id=DT-MVP-4015686)) 所提供的檔案啊！

而且從原始碼可以看出，預設的檔案提供者物件為 [\_hostingEnvironment.WebRootFileProvider](https://github.com/aspnet/AspNetCore/blob/d7bfbb5824b5f8876bcd4afaa29a611efc7aa1c9/src/Mvc/Mvc.Core/src/Infrastructure/VirtualFileResultExecutor.cs#L133)，也就是說，透過 [File(String, String)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file?view=aspnetcore-3.0&WT.mc_id=DT-MVP-4015686#Microsoft_AspNetCore_Mvc_ControllerBase_File_System_String_System_String_) 執行時，所載入的實體檔案，必須在所謂 [web root](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/index?view=aspnetcore-3.1&tabs=windows&WT.mc_id=DT-MVP-4015686#web-root) 目錄下，才能正常載入檔案！

> 請注意: 預設 ASP․NET Core 的 [web root](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/index?view=aspnetcore-3.1&tabs=windows&WT.mc_id=DT-MVP-4015686#web-root) 是位於專案根目錄下的 `wwwroot` 資料夾！

### 總結

在理解了這一切背景知識之後，我終於知道，原來透過 [ControllerBase.File](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.aspnetcore.mvc.controllerbase.file?view=aspnetcore-3.0&WT.mc_id=DT-MVP-4015686) 輔助方法，傳入 `virtualPath` 的時候，是以 [web root](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/index?view=aspnetcore-3.1&tabs=windows&WT.mc_id=DT-MVP-4015686#web-root) 路徑為起點 (`wwwroot`)，我只要將需要下載的檔案，放到 `wwwroot` 目錄下 (或任意子目錄)，就可以非常放心的寫出以下代碼，用 `wwwroot` 的相對路徑輸出檔案內容：

```csharp
public IActionResult GetFile()
{
    return File("IMG_0215.jpg", "image/jpeg");
}
```

> 當然，這種設計可以大幅提高安全性，避免被駭客掃描作業系統檔案的風險！

雖然最終的代碼，是那麼的簡單，但是官方文件完全沒有提及這些重要的背景知識，要能理解這行簡單的代碼，花了我好幾個小時的時間，但這過程卻幫我更加理解 ASP․NET Core 的其他部分，原來 File Provider 的概念，貫穿整個 ASP․NET Core 框架，有很多 Middleware 都有用到。如此一來，未來看文件與原始碼都會更有感覺了！

### 相關連結

-   [File Providers in ASP.NET Core | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/file-providers?view=aspnetcore-3.1&WT.mc_id=DT-MVP-4015686)
-   [Virtual Directory Inside Of ASP.NET Core App In IIS](https://www.c-sharpcorner.com/article/virtual-directory-inside-of-asp-net-core-app-in-iis/)
-   [File Providers In ASP.NET Core](https://www.c-sharpcorner.com/article/file-providers-in-asp-net-core/)
-   [ASP.NET Core 教學 - 取得網站根目錄](https://blog.johnwu.cc/article/asp-net-core-getting-web-root-path.html)
-   [\[鐵人賽 Day23\] ASP.NET Core 2 系列 - 上傳/下載檔案 | John Wu's Blog](https://blog.johnwu.cc/article/ironman-day23-asp-net-core-upload-download-files.html)
-   [Category:OWASP Top Ten 2017 Project - OWASP](https://owasp.org/www-project-top-ten/)
-   [Asp.Net Core Action Results Explained](http://hamidmosalla.com/2017/03/29/asp-net-core-action-results-explained/)
-   [InvalidOperationException on File return - Stack Overflow](https://stackoverflow.com/questions/53631178/invalidoperationexception-on-file-return)
