﻿# 使用 ASP.NET Core 2.2 開發 Web API 的注意事項

最近整理了一下 ASP.NET Core 2.2 Web API 在開發時的注意事項，魔鬼總是出現在細節裡，有些資訊沒遇到問題也不會特別去看，但有時間的時候，從頭到尾釐清一遍，其實還是很有幫助的。

### 選用正確的基底類別

-   ASP.NET Core Web API
    -   請務必繼承自 [ControllerBase](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.controllerbase?WT.mc_id=DT-MVP-4015686) 抽象類別
    -   ControllerBase 提供不少開發 Web API 所需的輔助方法
-   ASP.NET Core MVC
    -   請務必繼承自 [Controller](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller?WT.mc_id=DT-MVP-4015686) 類別
    -   Controller 繼承自 ControllerBase，但額外提供許多 Views 相關功能與輔助方法
-   混合式控制器 (Mixed Controller) (不建議)
    -   如果你要在一個控制器混用 MVC 與 Web API 的話，請繼承 [Controller](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.controller?WT.mc_id=DT-MVP-4015686) 類別

### ASP.NET Core Web API 常見的 Attributes 說明

-   宣告 Controller/Action **回應**的**狀態碼**
    -   [ProducesResponseTypeAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesresponsetypeattribute?WT.mc_id=DT-MVP-4015686)
    -   `[ProducesResponseType(StatusCodes.Status201Created)]`
    -   `[ProducesResponseType(StatusCodes.Status400BadRequest)]`
-   宣告 Controller/Action **回應**的**內容類型** (`Content-Type`)
    -   [ProducesAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.producesattribute?WT.mc_id=DT-MVP-4015686)
    -   `[Produces("application/json")]`
    -   `[Produces("application/json", "text/json")]`
    -   設定全站統一回應類型的方式: [https://stackoverflow.com/a/41464511/910074](https://stackoverflow.com/a/41464511/910074)
-   宣告 Controller/Action 可**接受**的**內容類型** (`Accept`)
    -   [ConsumesAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.consumesattribute?WT.mc_id=DT-MVP-4015686)
    -   `[Consumes("application/json")]`
-   宣告 Action 可**接受**的 **HTTP 動詞**與**路由範本**(route template)
    -   [HttpGetAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpgetattribute?WT.mc_id=DT-MVP-4015686)
        -   `[HttpGet]`
        -   `[HttpGet("{id}")]`
    -   [HttpPostAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppostattribute?WT.mc_id=DT-MVP-4015686)
        -   `[HttpPost]`
        -   `[HttpPost("")]`
    -   [HttpDeleteAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpdeleteattribute?WT.mc_id=DT-MVP-4015686)
        -   `[HttpDelete]`
        -   `[HttpDelete("{id}")]`
    -   [HttpPutAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.httpputattribute?WT.mc_id=DT-MVP-4015686)
        -   `[HttpPut]`
        -   `[HttpPut("{id}")]`
    -   [HttpPatchAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.httppatchattribute?WT.mc_id=DT-MVP-4015686)
        -   `[HttpPatch]`
        -   `[HttpPatch("{id}")]`
-   宣告 Controller **屬性路由**的**預設前置詞** (Route Prefix)
    -   [RouteAttribute](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routeattribute?WT.mc_id=DT-MVP-4015686)
    -   `[Route("api/[controller]")]`
-   宣告 Controller 啟用常見的 API 特性 (API-specific behaviors)
    -   [ApiController](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.mvc.apicontrollerattribute?WT.mc_id=DT-MVP-4015686)
    -   `[ApiController]`
-   套用 `[ApiController]` 屬性之後，將會啟用以下特性：
    -   必須使用**屬性路由**(Attribute Routing)
        
    -   只要發生**模型驗證**失敗，就會自動回應 HTTP 400 (Bad Request)，並且以 [ValidationProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.validationproblemdetails?WT.mc_id=DT-MVP-4015686) 型別[回應](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/?WT.mc_id=DT-MVP-4015686&view=aspnetcore-2.2#default-badrequest-response)，預設的 JSON 結構如下：
        
        ```json
        {
            "errors": {
                "name": [
                    "The name field is required."
                ]
            },
            "title": "One or more validation errors occurred.",
            "status": 400,
            "traceId": "80000093-0002-fe00-b63f-84710c7967bb"
        }
        ```
        
        這也意味著以下程式碼可以完全從每一個 Action 中移除：
        
        ```csharp
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        ```
        
        你可以[關閉驗證失敗時自動 HTTP 400 回應](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/?WT.mc_id=DT-MVP-4015686&view=aspnetcore-2.2#disable-automatic-400)
        
        ```csharp
        services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
            .ConfigureApiBehaviorOptions(options =>
            {
                // 關閉驗證失敗時自動 HTTP 400 回應
                options.SuppressModelStateInvalidFilter = true;
            });
        ```
        
        也可以[自訂驗證錯誤時的回應訊息](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/?WT.mc_id=DT-MVP-4015686&view=aspnetcore-2.2#customize-badrequest-response)
        
        ```csharp
        services.AddMvc()
           .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
           .ConfigureApiBehaviorOptions(options =>
           {
              options.InvalidModelStateResponseFactory = context =>
              {
                 var problemDetails = new ValidationProblemDetails(context.ModelState)
                 {
                    Type = "https://contoso.com/probs/modelvalidation",
                    Title = "One or more model validation errors occurred.",
                    Status = StatusCodes.Status400BadRequest,
                    Detail = "See the errors property for details.",
                    Instance = context.HttpContext.Request.Path
                 };
        
                 return new BadRequestObjectResult(problemDetails)
                 {
                    ContentTypes = { "application/problem+json" }
                 };
              };
            });
        ```
        
        > 如果你想在 `InvalidModelStateResponseFactory` 工廠方法中寫些 Log 訊息，可以參考 [How to log automatic 400 responses on model validation errors](https://github.com/aspnet/AspNetCore.Docs/issues/12157#issuecomment-487756787) 這篇討論中提供的範例程式。
        
    -   繫結 Action 參數時會自動套用模型繫結的預設規則
        
        -   **複雜型別**預設就會自動套用 `[FromBody]` 屬性，但有些特殊目的的型別是例外，例如：[IFormCollection](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.iformcollection?WT.mc_id=DT-MVP-4015686) and [CancellationToken](https://learn.microsoft.com/dotnet/api/system.threading.cancellationtoken)
        -   **參數型別**如果是 [IFormFile](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.iformfile?WT.mc_id=DT-MVP-4015686) 或 [IFormFileCollection](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.iformfilecollection?WT.mc_id=DT-MVP-4015686) 的話，會自動套用 `[FromForm]` 屬性
        -   任何**參數名稱**如果剛好等於**路由範本**(route template)的**路由參數名稱**，就會自動套用 `[FromRoute]` 屬性
        -   **簡單模型**或**任何其他參數**，全部都會自動套用 `[FromQuery]` 屬性
        -   任何預設規則外的模型繫結，都需要明確指定以下任何一個屬性：
            -   `[FromBody]` - 從 Request body 取值
            -   `[FromForm]` - 從 Request body 中的表單資料取值
            -   `[FromHeader]` - 從 Request header 取值
            -   `[FromQuery]` - 從 Query String 取值
            -   `[FromRoute]` - 從目前要求的路由變數取值
            -   `[FromServices]` - 從 DI 容器中取得服務物件
    -   當設定為 `CompatibilityVersion.Version_2_2` [相容模式](https://learn.microsoft.com/zh-tw/aspnet/core/mvc/compatibility-version?WT.mc_id=DT-MVP-4015686)後，你可以在組件層級套用 `[ApiController]` 屬性，如下範例：
        
        ```csharp
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
        ```
        
        ```csharp
        [assembly: ApiController]
        namespace WebApiSample
        {
            public class Startup
            {
                ...
            }
        }
        ```
        

### 全新的 HTTP 錯誤回應模型

如果你將 MVC 相容層級設定為 `services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)` 的話，預設所有的錯誤都會遵照 [RFC 7807 specification](https://tools.ietf.org/html/rfc7807) 的規定進行回應，從 ASP.NET Core 2.1 開始建立了一個 [ProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?WT.mc_id=DT-MVP-4015686) 模型類別，專門用來描述各種不同的 HTTP API 錯誤資訊。

> 從 ASP.NET Core 2.2 開始，ASP.NET Core 才正式將所有錯誤回應改用 [ProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?WT.mc_id=DT-MVP-4015686) 模型類別輸出！如果你想維持以前的預設訊息格式 ([HttpError](https://learn.microsoft.com/en-us/dotnet/api/system.web.http.httperror?WT.mc_id=DT-MVP-4015686))，可以將 [相容模式](https://learn.microsoft.com/zh-tw/aspnet/core/mvc/compatibility-version?WT.mc_id=DT-MVP-4015686) 調整為 `CompatibilityVersion.Version_2_1`，如下範例：
> 
> ```csharp
> services.AddMvc()
>     .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
> ```

當你的 Action 程式這樣寫：

```csharp
[HttpGet("{id}")]
public IActionResult Get(int id)
{
    return NotFound();
}
```

當回應 HTTP 404 的時候，其 Response Body 內容如下：

```json
{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
  "title": "Not Found",
  "status": 404,
  "traceId": "8000001c-0000-f900-b63f-84710c7967bb"
}
```

這個問題回應的預設訊息內容是可以自定的，你可以透過調整 `ClientErrorMapping` 屬性來改變這個內容，請參考以下 `ConfigureServices()` 中的設定：

```csharp
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressConsumesConstraintForFormFileParameters = false;
        options.SuppressInferBindingSourcesForParameters = false;
        options.SuppressModelStateInvalidFilter = false;

        // 設定 Client Errors (4xx) 的 Title 與 Link 內容
        options.SuppressMapClientErrors = false;
        options.ClientErrorMapping[404].Title = "找不到網頁";
        options.ClientErrorMapping[404].Link = "https://httpstatuses.com/404";
    });
```

設定完後，其回應內容如下：

```json
{
  "type": "https://httpstatuses.com/404",
  "title": "找不到網頁",
  "status": 404,
  "traceId": "80000044-0001-fb00-b63f-84710c7967bb"
}
```

如果你想要完全關閉**用戶端錯誤**(Client Errors)的任何訊息，則可以做出以下調整：

```csharp
services.AddMvc()
    .SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
    .ConfigureApiBehaviorOptions(options =>
    {
        options.SuppressMapClientErrors = true;
    });
```

### 相關連結

-   [Create web APIs with ASP.NET Core | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/web-api/?WT.mc_id=DT-MVP-4015686)
-   [How to log automatic 400 responses on model validation errors · Issue #12157 · aspnet/AspNetCore.Docs](https://github.com/aspnet/AspNetCore.Docs/issues/12157#issuecomment-487756787)
-   [ApiBehaviorOptions.SuppressUseValidationProblemDetailsForInvalidModelStateResponses Property (Microsoft.AspNetCore.Mvc) | Microsoft Docs](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressusevalidationproblemdetailsforinvalidmodelstateresponses?WT.mc_id=DT-MVP-4015686)
    -   我研究了好久，依然不知道這個屬性到底有啥用？先記錄起來日後備查！
    -   [SuppressUseValidationProblemDetailsForInvalidModelStateResponses not working #4792](https://github.com/aspnet/AspNetCore/issues/4792)
    -   [Suppress validation errors in ASP.NET Core's automatic 400 response - Stack Overflow](https://stackoverflow.com/questions/55811421/suppress-validation-errors-in-asp-net-cores-automatic-400-response)
    -   [Default BadRequest response](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/?WT.mc_id=DT-MVP-4015686#default-badrequest-response)
