﻿# ASP.NET Core Web API 遭遇 No route matches the supplied values 的問題

在開發 ASP․NET Core Web API 的 `[HttpPost]` 動作方法用來建立資料時，我們可以使用 `CreatedAtAction` 或 `CreatedAtRoute` 來回應訊息，但是可能會遇到 `No route matches the supplied values` 的問題，這篇文章我來說說問題發生的原因與解決方案。

![API](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/868db14baafc29cefe9d-232968641-aea1e102-7f3d-4532-bcaa-7eca3cee7aa5.jpg)

### 建立範例 Web API 專案

以下我們用 .NET 7 為範例：

1.  建立 `api1` 專案
    
    ```bash
    dotnet new webapi -n AspNetCoreWebApiEFDemo && cd AspNetCoreWebApiEFDemo
    ```
    
2.  加入 `.gitignore` 檔案
    
    ```bash
    dotnet new gitignore
    ```
    
3.  將專案加入 Git 版控
    
    ```bash
    git init && git add . && git commit -m "Initial commit"
    ```
    
4.  在資料庫中建立 [ContosoUniversity](https://gist.githubusercontent.com/doggy8088/2a2f7075d49b3814d19513426ede3549/raw/ab95b323425ff98e99b901793d0361d90439fb0b/ContosoUniversity.sql) 範例資料庫
    
    建議使用 SQL Server Express LocalDB 即可：`(localdb)\MSSQLLocalDB`
    
5.  安裝 `dotnet-ef` 全域工具 (.NET CLI Global Tool)
    
    ```bash
    dotnet tool update -g dotnet-ef
    ```
    
6.  安裝 Entity Framework Core 相關套件
    
    ```bash
    dotnet add package Microsoft.EntityFrameworkCore.SqlServer
    dotnet add package Microsoft.EntityFrameworkCore.Tools
    ```
    
    建立新版本
    
    ```bash
    git add . && git commit -m "Add EFCore NuGet packages"
    ```
    
7.  透過 `dotnet-ef` 快速建立 EFCore 模型類別與資料內容類別
    
    ```bash
    dotnet ef dbcontext scaffold "Server=(localdb)\MSSQLLocalDB;Database=ContosoUniversity;Trusted_Connection=True;MultipleActiveResultSets=true" Microsoft.EntityFrameworkCore.SqlServer -o Models
    dotnet build
    ```
    
    在透過 `dotnet ef` 產生程式碼之後，必須先建置專案，雖然建置會成功，但是卻會看到**警告訊息**如下：
    
    ![warning CS1030: #warning: 'To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.'](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/f6ae53886ed3fe5507bc-92549573-29de5000-f28c-11ea-82cb-d064b0503bbc.jpg)
    
    ```
    Models\ContosoUniversityContext.cs(32,10): warning CS1030: #warning: 'To protect potentially sensitive information in your connection string, you should move it out of source code. See http://go.microsoft.com/fwlink/?LinkId=723263 for guidance on storing connection strings.' [G:\Projects\api1\api1.csproj]
    ```
    
    這個警告訊息主要是跟你說，在透過 `dotnet-ef` 工具產生程式碼的時候，會順便將「連接字串」一定產生在 `ContosoUniversityContext.cs` 原始碼中，建議你手動將這段程式碼移除。
    
    ![To protect potentially sensitive information in your connection string, you should move it out of source code.](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/879f776b3d763e67528c-232923126-e797df55-5a48-4006-9951-d6a8192d28df.png)
    
    上圖修改完後的程式碼如下：
    
    ```csharp
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {}
    ```
    
    建立新版本
    
    ```bash
    git add . && git commit -m "Create EFCore models and dbcontext classes using dotnet-ef"
    ```
    
8.  調整 ASP․NET Core 的 `Program.cs` 並對 `ContosoUniversityContext` 設定 DI
    
    ```csharp
    using AspNetCoreWebApiEFDemo.Models;
    using Microsoft.EntityFrameworkCore;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // 加入這段程式碼
    builder.Services.AddDbContext<ContosoUniversityContext>(options =>
        options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
    ```
    
9.  調整 ASP․NET Core 的 `appsettings.Development.json` 加入 `DefaultConnection` 連接字串
    
    ```json
    {
      "ConnectionStrings": {
          "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Initial Catalog=ContosoUniversity;Integrated Security=True"
      },
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      }
    }
    ```
    
    建立新版本
    
    ```bash
    git add . && git commit -m "Add DI for ContosoUniversityContext and ConnectionStrings"
    ```
    
10.  建置專案，確認可以正常編譯！
     
     ```bash
     dotnet build
     ```
     
11.  建立方案檔
     
     ```bash
     dotnet new sln
     dotnet sln add AspNetCoreWebApiEFDemo.csproj
     ```
     
     建立新版本
     
     ```bash
     git add . && git commit -m "Add Solution File for Visual Studio 2022"
     ```
     

### 測試 POST 建立資料的 API

上述**建立範例 Web API 專案**我上傳到 [AspNetCoreWebApiEFDemo](https://github.com/doggy8088/AspNetCoreWebApiEFDemo) 專案中，你也可以跳過這些步驟，直接 `git clone` 回來使用。

```bash
git clone https://github.com/doggy8088/AspNetCoreWebApiEFDemo.git
cd AspNetCoreWebApiEFDemo
```

1.  使用 Visual Studio 2022 開啟 `AspNetCoreWebApiEFDemo.sln` 方案檔
    
2.  透過 Visual Studio 2022 工具產生 `CoursesController` API 控制器
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/267a48f186235267e98b-232931557-ef5464c2-6b69-4a75-b31f-0550b91e3c65.png)
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/051d611900be19194d56-232932176-7f5b9fcb-e6fe-47c9-b89c-125654f258f3.png)
    
    ![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/3920bf7a0c0e153e30f5-232932757-30fac839-36cb-43a6-81d9-50f198321d55.png)
    
3.  我們先調整一下 `Models\Course.cs` 的 `Department` 導覽屬性
    
    原本的程式碼是：
    
    ```csharp
    public virtual Department Department { get; set; } = null!;
    ```
    
    我們將其改為 nullable 的版本，以免 HTTP POST 無法建立資料：
    
    ```csharp
    public virtual Department? Department { get; set; }
    ```
    
4.  透過 `curl` 呼叫 API 用 POST 建立一筆新資料到 `Courses` 資料表中
    
    ```bash
    curl -v -d '{ "title": "ASP.NET Core 6 開發實戰", "credits": 1, "departmentId": 5 }' -H 'Content-Type: application/json' https://localhost:7054/api/Courses
    ```
    
    其回應結果如下，代表資料建立成功：
    
    ```
    *   Trying 127.0.0.1:7054...
    * Connected to localhost (127.0.0.1) port 7054 (#0)
    * schannel: disabled automatic use of client certificate
    * ALPN: offers http/1.1
    * ALPN: server accepted http/1.1
    * using HTTP/1.1
    > POST /api/Courses HTTP/1.1
    > Host: localhost:7054
    > User-Agent: curl/8.0.1
    > Accept: */*
    > Content-Type: application/json
    > Content-Length: 75
    >
    < HTTP/1.1 201 Created
    < Content-Type: application/json; charset=utf-8
    < Date: Wed, 19 Apr 2023 00:49:18 GMT
    < Server: Kestrel
    < Location: https://localhost:7054/api/Courses/18
    < Transfer-Encoding: chunked
    <
    {"courseId":18,"title":"ASP.NET Core 6 開發實戰","credits":1,"departmentId":5,"department":null,"enrollments":[],"instructors":[]}* Connection #0 to host localhost left intact
    ```
    
    接著呼叫 GET 方法，取得 `Location` 指向的那筆資料的端點(Endpoint)
    
    ```bash
    curl https://localhost:7054/api/Courses/18
    ```
    
    回應結果如下：
    
    ```
    {"courseId":18,"title":"ASP.NET Core 6 開發實戰","credits":1,"departmentId":5,"department":null,"enrollments":[],"instructors":[]}
    ```
    
5.  由於 API 可以成功呼叫，我們就來看一下目前這段可以運作的 Code 長怎樣
    
    這裡的亮點在於 HTTP 201 Created 所需的 ActionResult 使用了 `CreatedAtAction` 來當成 ActionResult 的回傳值：
    
    ```csharp
    [HttpPost]
    public async Task<ActionResult<Course>> PostCourse(Course course)
    {
        if (_context.Courses == null)
        {
            return Problem("Entity set 'ContosoUniversityContext.Courses'  is null.");
        }
        _context.Courses.Add(course);
        await _context.SaveChangesAsync();
    
        return CreatedAtAction("GetCourse", new { id = course.CourseId }, course);
    }
    ```
    
    在 `CreatedAtAction` 呼叫的第一個參數 `GetCourse` 是同一個 Controller 中的 Action Name，而 `new { id = course.CourseId }` 則是路由值，而 `course` 則是要回傳在 Response Body 的 Payload。
    
    事實上，這個 Controller 有個 Action 就叫 `GetCourse`，請注意，以下「方法名稱」預設就等於 Action Name，這是從 MVC 直接繼承下來的傳統：
    
    ```csharp
    // GET: api/Courses/5
    [HttpGet("{id}")]
    public async Task<ActionResult<Course>> GetCourse(int id)
    {
        if (_context.Courses == null)
        {
            return NotFound();
        }
        var course = await _context.Courses.FindAsync(id);
    
        if (course == null)
        {
            return NotFound();
        }
    
        return course;
    }
    ```
    

### 重現有問題的實作

為了讓我的 API 可以更佳健壯(robust)，我打算做出以下調整：

1.  將所有非同步的方法都改成 `Async` 結尾
2.  替每個 Action 加上路由名稱(Route Name)
3.  路由名稱一律使用 `nameof` 語法取得字串值

現在，我的 `GetCourse(int id)` 與 `PostCourse(Course course)` 這兩個 Action 變更如下：

1.  `GetCourse` 改為 `GetCourseByIdAsync`
    
    我有替這個 Action 加上路由名稱(Route Name)為 `GetCourseByIdAsync`，但是你絕對想不到 `ActionName` 變成了什麼，因為你完全會看不出來的！我先說答案：是 `GetCourseById`，也就是非同步方法中常見的 `Async` 命名結尾，到了 Action Name 會自動移除，這是 MVC 需要的特性。
    
    ```csharp
    [HttpGet("{id}", Name = nameof(GetCourseByIdAsync))]
    public async Task<ActionResult<Course>> GetCourseByIdAsync(int id)
    {
        if (_context.Courses == null)
        {
            return NotFound();
        }
        var course = await _context.Courses.FindAsync(id);
    
        if (course == null)
        {
            return NotFound();
        }
    
        return course;
    }
    ```
    
2.  `PostCourse` 改為 `GetCourseByIdAsync`
    
    這時問題出現了，問題就出在 `CreatedAtAction` 這行的第一個參數，因為 ActionName 用 `GetCourseByIdAsync` 是不正確的，要用 `GetCourseById` 才對！
    
    ```csharp
    [HttpPost(Name = nameof(PostCourseAsync))]
    public async Task<ActionResult<Course>> PostCourseAsync(Course course)
    {
        if (_context.Courses == null)
        {
            return Problem("Entity set 'ContosoUniversityContext.Courses'  is null.");
        }
        _context.Courses.Add(course);
        await _context.SaveChangesAsync();
    
        return CreatedAtAction(nameof(GetCourseByIdAsync), new { id = course.CourseId }, course);
    }
    ```
    
    此時你可以用 POST 再建立一筆資料試試：
    
    ```bash
    curl -v -d '{ "title": "ASP.NET Core 6 開發實戰", "credits": 1, "departmentId": 5 }' -H 'Content-Type: application/json' https://localhost:7054/api/Courses
    ```
    
    你將發現以下 `No route matches the supplied values.` 錯誤訊息，掛掉的地方則是 `Microsoft.AspNetCore.Mvc.CreatedAtActionResult.OnFormatting` 方法：
    
    ```
    System.InvalidOperationException: No route matches the supplied values.
      at Microsoft.AspNetCore.Mvc.CreatedAtActionResult.OnFormatting(ActionContext context)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsyncCore(ActionContext context, ObjectResult result, Type objectType, Object value)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]()
    --- End of stack trace from previous location ---
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
    --- End of stack trace from previous location ---
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
      at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
      at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
      at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
      at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
      at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
      at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
      at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
    ```
    

### 解決方案

知道原因就好了，原來是 ActionName 不是我們想像的那個樣子，但說實在的，我們寫 ASP.NET Core Web API 的時候，根本用不太到**動作名稱**(Action Name)，改用**路由名稱**(Route Name)才是比較合理的寫法，所以解決方案有 3 種，而我覺得最漂亮的解法是最後一種：

1.  繼續用 `CreatedAtAction` 但設定正確的名稱
2.  繼續用 `CreatedAtAction` 但替 Action 加上 `[ActionName]` 屬性
3.  改用 `CreatedAtRoute` 設定路由名稱

以下是解決方案的原始碼：

1.  繼續用 `CreatedAtAction` 但設定正確的名稱
    
    ```csharp
    [HttpPost(Name = nameof(PostCourseAsync))]
    public async Task<ActionResult<Course>> PostCourseAsync(Course course)
    {
        if (_context.Courses == null)
        {
            return Problem("Entity set 'ContosoUniversityContext.Courses'  is null.");
        }
        _context.Courses.Add(course);
        await _context.SaveChangesAsync();
    
        return CreatedAtAction(nameof(GetCourseByIdAsync).Replace("Async", ""), new { id = course.CourseId }, course);
    }
    ```
    
    亮點在這裡：
    
    ```csharp
    nameof(GetCourseByIdAsync).Replace("Async", "")
    ```
    
2.  繼續用 `CreatedAtAction` 但替 Action 加上 `[ActionName]` 屬性
    
    我個人不太喜歡這種解決方案，因為 ASP.NET Web API 根本用不到 Action Name
    
    ```csharp
    [HttpGet("{id}", Name = nameof(GetCourseByIdAsync))]
    [ActionName(nameof(GetCourseByIdAsync))]
    public async Task<ActionResult<Course>> GetCourseByIdAsync(int id)
    {
        if (_context.Courses == null)
        {
            return NotFound();
        }
        var course = await _context.Courses.FindAsync(id);
    
        if (course == null)
        {
            return NotFound();
        }
    
        return course;
    }
    ```
    

-   改用 `CreatedAtRoute` 設定路由名稱
    
    這是我覺得最理想的解決方案，指定**路由名稱**才是王道！👍
    
    ```csharp
    [HttpPost(Name = nameof(PostCourseAsync))]
    public async Task<ActionResult<Course>> PostCourseAsync(Course course)
    {
        if (_context.Courses == null)
        {
            return Problem("Entity set 'ContosoUniversityContext.Courses'  is null.");
        }
        _context.Courses.Add(course);
        await _context.SaveChangesAsync();
    
        return CreatedAtRoute(nameof(GetCourseByIdAsync), new { id = course.CourseId }, course);
    }
    ```
    

### 總結

這個問題主要卡在你對 `CreatedAtAction` 與 `CreatedAtRoute` 的理解，Action 與 Route 由於定義不一樣，如果沒有 MVC 的基礎，要瞭解 Action Name 的特性其實不太直觀，所以我還是認為上述的第 3 種解決方案才是比較好的解決方案，寫 ASP.NET Core Web API 時不要再依賴 Action Name 來寫 Code 了！

### 相關連結

-   [No route matches the supplied values - JosephGuadagno.net](https://www.josephguadagno.net/2020/07/01/no-route-matches-the-supplied-values)
