---
# System prepended metadata

title: C# ASP.NET Core Web API

---

C# ASP.NET Core Web API
==
> 參考系列影片：[[凱哥寫程式] ASP.NET CORE WEB API 入門教學](https://www.youtube.com/playlist?list=PLneJIGUTIItsqHp_8AbKWb7gyWDZ6pQyz)
> 實作參考：[[Day21] C# MVC RESTful API (下) 實作RESTful API - C#&AspNetCore](https://yuhsiang237.github.io/2021/10/18/C-MVC-RESTful-API-%E4%B8%8B-%E5%AF%A6%E4%BD%9CRESTful-API/)
> 實作參考 + 完整資訊補充：[菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧](https://igouist.github.io/post/2021/05/newbie-2-webapi/)
> Swagger 相關：[ASP.NET Core Web API 入門心得 - 改善 Enum 註解](https://hackmd.io/@CloudyWing/rkUOPLxeC)
> 


## 建立專案
> for macOS
> 如果是 Visual Studio for mac ，安裝時 .NET 預設版本如果沒有8.0可選，請自行安裝 [.NET 8.0](https://dotnet.microsoft.com/zh-tw/download/dotnet/8.0)，最新版本的SDK有較完整功能可使用

>[!note] 
> - 如果是以 Visual Studio for Mac 開啟專案的，在建立專案時記得將<font color="orange"> **HTTPS Configuration / 針對 HTTPS 進行設定**</font> 的 <font color="orange">checkbox 移除</font>，否則執行時會遇到無法設定 https 憑證的問題
> - 參考頁面：[Opt-out of HTTPS/HSTS on project creation](https://learn.microsoft.com/en-us/aspnet/core/security/enforcing-ssl?view=aspnetcore-8.0&tabs=visual-studio%2Clinux-sles#opt-out-of-httpshsts-on-project-creation)

#### 按下 <font color="green">++執行++</font> 按鈕後，如果有跳出瀏覽器畫面，且上面有預設的 json 格式資料，就算安裝成功了。

### 創建第一支 HelloWorld API
1. 打開 Controllers 資料夾，裡面會看到預設的 controller.cs 檔案。
1. 在資料夾的地方<font color="green">按右鍵</font>，選 `新增 > 新增檔案 > ASP.NET CORE > WEB API 控制器`，<font color="green">更改檔案名稱</font>為 **HelloWorldController(.cs)**，按下<font color="green">建立</font>。
    - 檔案名稱就算沒有保留 Controller ，程式也可以執行，但這樣命名比較清楚。
    - for mac 版本預設打開時就會先建立好基本 CRUD 的框架。
1. 裡面會看到 ROUTES，這個就是預設的網址路徑: http://localhost:5154/api/helloworld
    ```csharp
    [Route("api/[controller]")]
    public class HelloWorldController : ControllerBase
    ```
    - `[controller]` 的部分就會是下面 class 的名稱(不含 controller)
    - 繼承自 `ControllerBase` 而非 `Controller`
        - ControllerBase 
            1. 專門為 Web API 設計的類別，`[APIController]` 會自動處理模型驗證和錯誤回應。
            2. 輕量級，不用處理 view 功能，效能更高。
            3. 較好的錯誤處理，可以使用 middleware 中介統一處理錯誤，不用在每個 controller 中重複編寫。
        - Controller 是支援 MVC 模式的控制器基類，適用於需要返回 HTML view 的應用程式。
        - **如果只需要純 API，使用 ControllerBase；如果需要返回 API 和 HTML，則使用 Controller。**
    - 網址不分大小寫
1. 將原本 [HttpGet] 預設的回傳值 Value1 Value2 改為 Hello World
    ```csharp
    // GET: api/values
    [HttpGet]
    public IEnumerable<string> Get()
    {
        return new string[] { "Hello", "World" };
    }
    ```    
1. 再回到瀏覽器重整頁面： http://localhost:5154/api/helloworld 就會看到 Hello World 了
    - windows 版本有熱重載，但 mac 似乎沒有(?)，所以檔案儲存後，還是需要重新執行才能看到頁面的變動。

## 開發工具
### 開發環境介紹
1. 方案 vs 專案
    - 一個方案底下可以有多個專案和類別庫
    - 每個專案都可以是一個 API
    - 類別庫：舉例->公司本身常用的類別庫就可以放入
1. 專案檔
    - 在專案資料夾按右鍵，點選編輯專案檔，會出現 XXX.csproj
        ```csharp
        <Project Sdk="Microsoft.NET.Sdk.Web">

          <PropertyGroup>
            <TargetFramework>net6.0</TargetFramework>
            <Nullable>enable</Nullable>
            <ImplicitUsings>enable</ImplicitUsings>
          </PropertyGroup>

        </Project>    
        ```
        - Nullable 設為 enable 時，IDE (Visual Studio) 會針對空值提出警告，可以改為 disable 關掉警告
        - ImplicitUsing 預設載入常用內容，不用在每個檔案都 Using 太多東西 ex：Using System
1. 設定檔 
    - Properties/launchSettings.json
        ```json
          "profiles": {
            "API": {
              "commandName": "Project",
              "launchBrowser": true,
              "launchUrl": "weatherforecast",
              "applicationUrl": "http://localhost:5154",
              "environmentVariables": {
                "ASPNETCORE_ENVIRONMENT": "Development"
              },
              "dotnetRunMessages": true
            },
        ```
        - 裡面有預設啟動的 port ( applicationUrl ) / launchUrl，都可以在這裡修改
        - 除了有預設 http 啟動方式，也有 iis 的設定
        :::info 
        Visual Studio for Mac 不支援 IIS Express，這是因為 IIS Express 是 Windows 環境中的一個工具，主要用於本地開發和測試 ASP.NET 應用程序。由於 Mac OS 不支援 IIS，因此在 Visual Studio for Mac 中無法使用 IIS Express。
        :::
1.  appsettings.json
    - 算是一個共用的字典，常用到的東西寫在這邊，其他程式需要用到再來這裡抓
    - 有兩個檔案：
        - 部署後 `appsettings.json`
        - 開發時 `appsettings.Development.json`
    - 開發環境下，會去讀取 `appsettings.Development.json` 的值去覆蓋 `appsettings.json` 的設定
1.  Program.cs
    - 算是設定檔，有需要用到的東西就在這裡載入
    - 註冊各種服務、Swagger 文件的設定 + UI 的調整
1.  Weatherforcast.cs
    - 範例檔的 class 檔案，後續用不到就可移除

### POSTMAN - API 測試工具
> 參考：
> [POST MAN 安裝+基本操作](https://blog.talllkai.com/ASPNETCore/2021/03/14/RESTfulAPIAndPostman)

- ==GET==
- ==POST==
- 


### LINQ
- LINQ = Language Integreted Query 語言整合查詢
- 使用 LINQ 可以查詢各種不同對象，可以減少學習成本並提升方便性。
    - 可用來查詢 SQL / XML / Object
- LINQ 有兩種寫法：
    1. 宣告式 Declarative： 
        ```csharp
        var result = from a in newslist
                     where a.Title == "今天天氣很好"
                     select a;
        ```
    1. 編程式 Imperative：
        ```csharp
        var result = NewsList.Where(a => a.Title == "今天天氣很好");
        ```
## 資料庫
### SQL Server 安裝
### EF Core 資料庫連線
- EF Core = Entity Framework Core
    - 連線操作方式有兩種：
        1. Database First:
            先在資料庫建立好資料庫與表格欄位，再到 visual studio 這邊下建立資料庫物件指令，則會在專案中產生相關對應的資料庫程式檔。
        2. Code First:
            直接在專案中，以程式的方式寫出資料庫的表格和欄位，再下指令，就會在資料庫建立好對應的資料庫和表格欄位。

- 需要先安裝二個套件：資料庫 & Tool
    > 先到 `專案資料夾>相依性` 按右鍵，管理 Nuget 套件，搜尋套件名稱後安裝
    > 原本先用 postgresql，後來有成功用 docker 執行 sql server ，可看 [[note] 使用 docker 在 mac 上運行 SQL Server](/-5tH7GHlS5GjdiYCaRjfMA)

    - ~~Microsoft.EntityFrameworkCore.SqlServer~~ (sql server 資料庫，我先替換為 Postgresql)
    - ~~Microsoft.EntityFrameworkCore.InMemory~~ (暫存資料庫)
    - Npgsql.EntityFrameworkCore.PostgreSQL (postgresql 資料庫)
    - Microsoft.EntityFrameworkCore.Tools (下指令時會使用到這個套件)
            
#### 使用 Database First
> 參考：[Entity Framework Core資料庫連線，使用Database First](https://blog.talllkai.com/ASPNETCore/2021/04/13/DatabaseFirst)
> - 有提到 **依賴注入**
        
- 資料庫設定(以 postgres 為例):
    - 先到 PGadmin 建立伺服器、資料庫、表格
    - Server 名稱隨意
    - Database / User / Password 這幾個要記得
    - Host 先設為 localhost 或 127.0.0.1
    - 可以先建立三筆資料(後續測試連線用)
- Scaffold-DbContext
    >[!Note]資料庫變動
    >後續只要資料庫有變動，重新執行一次 **dbcontext scaffold** 完整指令即可
    
    1. 在 package manager console 上執行
        ```
        $ Scaffold-DbContext "Host=localhost;Database=YourDatabase;Username=YourUsername;Password=YourPassword" Npgsql.EntityFrameworkCore.PostgreSQL -OutputDir Models
        ```
    1. 在終端機上執行
        ```
        $ dotnet ef dbcontext scaffold "Host=localhost;Database=YourDatabase;Username=YourUsername;Password=YourPassword" Npgsql.EntityFrameworkCore.PostgreSQL -OutputDir Models
        ```
    - 參數：
        - `TrustServerCertificate=true`：信任伺服器憑證
        - `-OutputDir Models` or `-o Models`:將檔案輸出到Models資料夾
        - `-Force` 會覆蓋原本相同名稱的資料夾，在更新資料庫時會使用到
        - `-NoOnConfiguring` DbContext 不要產生 OnConfiguring 實體化資料庫的設定(以前使用 using 才需要這個寫法)。因為++改以依賴注入處理++，所以這段可以省略。如果沒加上這個參數，會自動產生以下程式碼：
            ```csharp
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            {
                if (!optionsBuilder.IsConfigured)
                {
                    optionsBuilder.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=sherry85;TrustServerCertificate=true");
                }
            }
            ```
    - Scaffold 指令是用來將現有資料庫生成出資料庫模型及上下文 (DbContext)
- 將連線字串寫入 appsettings.json
    ```json
    "ConnectionStrings": {
        "DefaultConnection": "Host=localhost;Database=postgres;Username=postgres;Password=your_password"
  }
    ``` 
- DI 注入，寫入 Program.cs
    ```csharp
    builder.Services.AddDbContext<postgresContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
    ```
    - `postgresContext` 看Models 資料夾建立時生成的檔案名
    - `UseNpgsql` 依資料庫，如果是 SQL Server 就會是 UseSqlServer
    - `Configuration.GetConnectionString("DefaultConnection")` 可以從 appsettings.json 內抓出 `"DefaultConnection":`後的內容
---
#### 測試前面設定是否成功
- 建立新的 PostController.cs
    ```csharp
    namespace API.Controllers
    {
        [Route("api/[controller]")]
        [ApiController]
        public class PostController : Controller
        {
            private readonly postgresContext _postgresContext;
            
            // 取得依賴注入物件
            public PostController(postgresContext postgresContext)
            {
                _postgresContext = postgresContext;
            }

            [HttpGet]
            public IEnumerable<Post> Get()
            {
                return _postgresContext.Posts;
            }
        }
    }
    ```
    - `[APIController]` 如果套用在組件時，組件中所有控制器都會套用這個屬性。
        > 官方文件：[組件上的屬性](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/?view=aspnetcore-8.0#attribute-on-an-assembly)
        > 在 .NET 中，組件是一個編譯後的單位，通常是 .dll 或 .exe 檔案。它是由 C# 檔案編譯而成的，並且可以包含類、接口、資源等。
    - `private readonly postgresContext _postgresContext;` 第一個 postgresContext 是資料庫名稱，第二個_postgresContext 是變數名稱，習慣加上底線
    - Constructor 建構類別 (和 class 同名且類型為public)，當在 program.cs 內有寫到依賴注入設定時，在這裏就是去取得依賴注入物件
- 按下啟動按鈕，在瀏覽器上輸入路徑，確認前面輸入的資料是否成功顯示

#### 使用 Code First

- 先建立 Models 資料夾
- 建立資料表(table)
    - `新增 > 新增檔案 > 一般 > 類別是空的` 
    - 檔名設為 News (以下 table 以 News 為例)
- 建立資料庫物件
    - `新增 > 新增檔案 > 一般 > 類別是空的` 
    - 檔名設為 webContext (資料庫名稱+Context，取名慣例)
- 資料庫物件寫法：
    ```csharp
    using Microsoft.EntityFrameworkCore;

    namespace API.Models
    {
        public class webContext : DbContext
        {
            public webContext(DbContextOptions<webContext>options) : base(options)
            {

            }
            // 告訴資料庫這裡會有一個 News 資料表
            public DbSet<News> News { get; set; }
            
            // 詳細定義資料表欄位
            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<News>(entity =>
                {
                    entity.Property(e => e.Newsid).HasDefaultValueSql("(NewId())");
                    entity.Property(e => e.Title).IsRequired().HasMaxLength(250);
                    entity.Property(e => e.Content).IsRequired();
                    entity.Property(e => e.Enable).HasDefaultValue(true);
                    entity.Property(e => e.EndDateTime)
                        .HasDefaultValueSql("(getDate())")
                        .HasColumnType("dateTime");
                    entity.Property(e => e.InsertDateTime)
                        .HasDefaultValueSql("(getDate())")
                        .HasColumnType("dateTime");
                    entity.Property(e => e.StartDateTime)
                        .HasDefaultValueSql("(getDate())")
                        .HasColumnType("dateTime");
                    entity.Property(e => e.UpdateDateTime)
                        .HasDefaultValueSql("(getDate())")
                        .HasColumnType("dateTime");
                });
            }
        }
    }
    ```
    1. 引入 `Microsoft.EntityFrameworkCore`
    2. class webContext 繼承 DbContext
    3. 寫上資料庫內要建立的資料表 (資料表內容則是依 News.cs 檔案)
    4. 定義資料表欄位詳細格式
- 將連線字串寫入 appsettings.json
    ```json
  "ConnectionStrings": {
        "DefaultConnection": "Server=localhost;Database=web;User Id=your_username;Password=your_password;TrustServerCertificate=True;"
  }
    ``` 
    - 這邊要注意 不同資料庫的連線字串 需要的欄位名稱不同
- DI 注入，寫入 Program.cs
    ```csharp
    builder.Services.AddDbContext<postgresContext>(options => 
        options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
    ```
    - `postgresContext` 看Models 資料夾建立時生成的檔案名
    - `UseNpgsql` 依資料庫，如果是 SQL Server 就會是 UseSqlServer
    - `Configuration.GetConnectionString("DefaultConnection")` 可以從 appsettings.json 內抓出 `"DefaultConnection":`後的內容
    - 建議也放在註冊控制器 `builder.Services.AddControllers();` 之前
    - <font color="green">**一定要放在 `builder.Build` 指令前，這個指令是在創建一個 app 實例，所以所有服務都要在此之前註冊完成**</font>
- 生成描述檔
    - `$ dotnet ef migrations add InitialCreate`
    - `InitialCreate` 是描述檔名稱
- 更新資料庫
    - `$ dotnet ef database update`
    - 會產生兩個資料表，其中一個是紀錄更新變化的資料表
- 如果要指定某個資料表，可用參數 `--context webContext`

#### 初始資料建立 (假資料) SeedData
- 在 Models 資料夾內建立 SeedData.cs 檔案
    ```csharp
    using Microsoft.EntityFrameworkCore;

    namespace API.Models
    {
        public class SeedData
        {
            public static void Initialize(IServiceProvider serviceProvider)
            {
                using (var context = new webContext(serviceProvider.GetRequiredService<DbContextOptions<webContext>>()))
                {
                    // 如果 News 內沒有任何資料的話，建立以下資料
                    if (!context.News.Any())
                    {
                                            context.News.AddRange(
                                            new News
                                            {
                                                Title = "第一個新聞",
                                                Content = "新聞內容1",
                                                InsertEmployeeId = 1,
                                                UpdateEmployeeId = 1,
                                                Click = 0,
                                                Enable = true
                                            },
                                            new News
                                            {
                                                Title = "第二個新聞",
                                                Content = "新聞內容2",
                                                InsertEmployeeId = 2,
                                                UpdateEmployeeId = 1,
                                                Click = 0,
                                                Enable = true
                                            },
                                            new News
                                            {
                                                Title = "第三個新聞",
                                                Content = "新聞內容3",
                                                InsertEmployeeId = 1,
                                                UpdateEmployeeId = 3,
                                                Click = 0,
                                                Enable = true
                                            });

                        context.SaveChanges();
                    };
                }
            }
        }
    }
    ```
- 在 program.cs 內標明，當應用程式啟動時，初始化資料庫並填入 SeedData
    ```csharp
    var app = builder.Build();

    // 告訴資料庫要執行 SeedData 內的 Initialize
    using (var scope = app.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        SeedData.Initialize(services);
    }
    ```

## 取得資料列表 & DTO
1. **IEnumerable<T>** 寫法
    > <T> = <Type>
    > 關於 IEnumerable : [傳回 IEnumerable<T> 或 IAsyncEnumerable<T>](https://learn.microsoft.com/zh-tw/aspnet/core/web-api/action-return-types?view=aspnetcore-8.0#return-ienumerablet-or-iasyncenumerablet)
    
    - 
    ```csharp
    [HttpGet]
    public IEnumerable<Post> Get()
    {
        return _postgresContext.Posts;
            // 資料庫物件.資料表名稱
    }
    ```
2. **IActionResult** 寫法 <font color="red">*</font>
    - 可以靈活去設定返回的響應內容和狀態碼
    ```csharp
    [HttpGet]
    public IActionResult GetTodos()
    {
        var todos = _todosContext.Todos.ToList();

        var result = todos.Select(todo => new TodoDtos
        {
            Id = todo.Id,
            Content = todo.Content,
            CompletedAt = todo.CompletedAt
        }).ToList();

        return Ok(result);
    }
    ```
### 只顯示部分欄位
- 只想顯示部分資料給使用者
- 節省記憶體容量
- 宣告式寫法:
    ```csharp
    [HttpGet]
    public IEnumerable<object> Get()
    {
        var result = from a in _postgresContext.Posts
                     select new
                     (
                         a.Title,
                     );
        return result;
    }
    ```
    - 但因為C# 中，所有型別的基礎都是 object。object 本身並非明確定義的型別，所以會缺乏實際數據的類型，且缺乏可讀性。
### DTO
> DTO = Data Transfer Object 資料傳輸物件
> 參考：[建立資料傳輸物件](https://learn.microsoft.com/zh-tw/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5)

- 用於定義如何透過網路傳送資料
- 建立DTO檔去定義類型，以避免後續維護困難
- 避免暴露過多資訊給使用者
- 減少 API 傳送大小
- <font color="red">算是去定義一個強型別 (?)</font>

#### 建立 DTO 檔案
1. 建立 Dtos 資料夾
    - 後續可能還會有其他 DTO 檔案
1. 建立 PostDto.cs 檔案
    ```csharp
    namespace API.Dtos
    {
        public class PostDto
        {
            public int Id { get; set; }
            public string Title { get; set; }
            public string Type { get; set; }
        }
    }
    ```
    >[!Note]{ get; set; }
    > - `get/set` 訪問器，分別用於 **讀取屬性的值** 和 **設置屬性的值**。
    > - `set` 訪問器可以添加驗證邏輯。
    > - **自動實現的屬性**：`{ get; set; }`，無需顯式定義私有字段。
    > - 使用 `private set` 和 `readonly`： `{ get; private set; } `，或 `readonly` 只使用 `get` 訪問器而不使用 `set` 訪問器，屬性在物件創建後就不會被更改。
    > ``` csharp
    > public class Account
    >    {
    >        private decimal _balance;
    >
    >        public decimal Balance
    >        {
    >            get { return _balance; }
    >            set
    >            {
    >                if (value < 0)
    >                    throw new ArgumentException("Balance cannot be negative.");
    >                _balance = value;
    >            }
    >        }
    >    }
    > ```
1. PostController.cs 檔案
    ```csharp
    [HttpGet]
    public IEnumerable<PostDto> Get() // 清楚定義會回傳一個集合，包含多個 PostDto 實例
    {
        var result = from a in _postgresContext.Posts
                     select new PostDto // 使用 new 關鍵字創建一個 PostDto 實例
                     {
                         Id = a.Id,
                         Title = a.Title,
                         Type = a.Type,
                     };
        return result;
    }
    ```
    
## 取得指定資料
1. 使用 Dto 寫法
    ```csharp
    // GET /<controller>/{id}
    [HttpGet("{id}")]
    public PostDto Get(int id)
    {
        var result = (from a in _postgresContext.Posts
                      where a.Id == id
                      select new PostDto
                      {
                         Id = a.Id,
                         Title = a.Title,
                         Type = a.Type,
                      }).SingleOrDefault(); // 只撈出一筆的意思
        return result;
    }
    ```
    2. **IActionResult** 寫法 <font color="red">*</font>
    ```csharp
    [HttpGet("{id}")]
    public IActionResult GetTodo(int id)
    {
        var todo = _todosContext.Todos.SingleOrDefault(t => t.Id == id);
        if (todo == null)
    {
        return NotFound();
    }

    var result = new TodoDtos
    {
        Id = todo.Id,
        Title = todo.Title,
        IsCompleted = todo.IsCompleted
    };

    return Ok(result);
    }
    ```    
- `.SingleOrDefault()` 只取得一筆資料，且如果沒有這個 id，也不會報錯(只是回傳 null)
- `.Single()` 只取一筆資料，如果沒有取得資料，會報錯    
## 新增資料
- 使用 IActionResult，可以靈活設定返回的狀態碼和響應內容
- `[FromBody]` 是指請求主體
    ```csharp
    [HttpPost]

    //IActionResult 可以靈活去設定返回的響應內容和狀態碼
    public IActionResult CreateTodo([FromBody]TodoDtos todoDtos)
    {
        if (todoDtos == null || string.IsNullOrEmpty(todoDtos.Title))
        {
            // 返回 400
            return BadRequest("未輸入要新增的待辦事項");
        }

        var todo = new Todos
        {
            Title = todoDtos.Title,
            IsCompleted = todoDtos.IsCompleted
        };

        _todosContext.Todos.Add(todo);
        _todosContext.SaveChanges();

        // 返回 201
        return CreatedAtAction(nameof(GetTodo), new { id = todo.Id }, todo);
    }
    ```
- 如果是 void 的寫法，在新增完成後不會返回值，只會顯示 204 no content
    ```csharp
    public void Post([FromBody] string value){}
    ```
    
    
## Postman 測試
### GET
### POST 
- ++Body++ 新增內文
    ```json
    {
        "Title": "看電影",
        "IsCompleted": false
    }
    ```
- 如果一開始得到 415 ，可能是沒有設置標頭的關係
    - ++Headers++ 新增 **Key: <font color="orange">Content-Type</font>** / **Value: <font color="orange">application/json</font>**
    
    
## User 登入登出
    
### 1. 建立模型類別
```csharp
using System.ComponentModel.DataAnnotations;

namespace TodoAPI.Models
{
	public class Users
	{
		[Key]
		public Guid Id { get; set; }

		[Required]
		[EmailAddress]
		public string Email { get; set; }

		[Required]
		public string PasswordHash { get; set; } // 加密用戶密碼
	}
}
```
### 2. 建立 DTO 類別
```csharp
public class UserDto
{
    public string Email { get; set; }
    public string Password { get; set; }
}
```
```csharp
public class TokenDto
{
    public string Token { get; set; }
    public DateTime Expiration { get; set; }
}
```
### 3. 建立用戶服務(Service)
```csharp

```
- #### Task / Task<T>
    Task 是一種型別，用來表示一個異步操作的結果，通常用於表示一個正在進行的操作的狀態，並在操作完成時返回結果。
    - `Task` 表示一個不返回結果的異步操作。
    - `Task<T>` 表示一個返回結果的異步操作，其中 T 為返回的類型。
    
### 4. 建立控制器(Controller)
```csharp

```
    
## JWT 驗證
> JWT = Json Web Token

> - [JWT 首頁](https://jwt.io/)
> - [什麼是 JWT ?](https://5xcampus.com/posts/what-is-jwt?source=post_page-----e0b25d8ee121--------------------------------)
> - youtube 教學：[Secure Your .NET API in 15 Minutes: JWT Authentication Tutorial](https://youtu.be/6DWJIyipxzw?si=PYfufxG07jDNVi70)    
> - [JWT Authentication in C# .NET Core 7 Web API — .NET Essentials](https://medium.com/@sajadshafi/jwt-authentication-in-c-net-core-7-web-api-b825b3aee11d)
> - [JWT Authentication in ASP.NET Core Web API](https://dotnettutorials.net/lesson/jwt-authentication-in-asp-net-core-web-api/)

> 參考：
> - [｜從0到1的建立Web API專案｜以 JWT 功能 + Swagger 為例](https://medium.com/appxtech/%E5%BB%BA%E7%AB%8Bweb-api%E5%B0%88%E6%A1%88%E5%BE%9E%E7%84%A1%E5%88%B0%E6%9C%89-jwt-%E5%BB%BA%E7%AB%8B-swagger-%E6%90%AD%E9%85%8D-jwt-e0b25d8ee121)
    
- JWT 組成：
    1. HEADER
    2. PAYLOAD
    3. VERIFY SIGNATURE

### JWT 設定參數
- 在 `appsettings.cs` 裡面放上 JWT 設定參數
    ```csharp
    "JwtSettings": {
        "Issuer": "TestAPI",
        "SignKey": "1qaz@WSX",
        "ExpireSec": 30000
    }
    ```
    1. Issuer 發行人
    1. SignKey 用來加密的金鑰字串
    1. ExpireSec 有效期限
    > [!Tip] 如何讀取到 appsettings.cs 內的字串
    > 可使用 **Configuration** / **IConfiguration**
        > > Confuguration - `program.cs`
        > > IConfuguration - Service / Middleware / Controller 等其他地方
    
### 建立 JWT TOKEN Provider

#### 安裝套件
- Nuget 管理套件:
    > 如果無法從 Nuget 管理套件的介面操作，可以在終端機下指令 `$dotnet add package 套件名`
    - `Microsoft.AspNetCore.Authentication.Bearer`
    - `Microsoft.AspNetCore.Identity.EntityFrameworkCore`
    - `Microsoft.AspNetCore.Identity`
    - `System.IdentityModel.Tokens.Jwt`
    

- 安裝後至 `.csproj` 檔案確認，是否有更新
    ```
    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.35" />
        <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
    </ItemGroup>
    ```

####

    
### 建立 Provider

    
    
## Swagger 文件
    
- Swagger 是 API 的文件產生器，具有 UI 介面，內容包含路由、方法、參數、回傳值
- .NET 中有兩個主要的 Swagger 工具包，分別是 **SwashBuckle** 和 **Nswag** (下面會以 SwashBuckle 為例)
    > 可參考：
    > SwashBuckle: [菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧](https://igouist.github.io/post/2021/05/newbie-4-swagger/)
    > Nswag: [[Day09]使用Swagger自動建立清晰明瞭的REST API文件 - 我與 ASP.NET Core 的 30天](https://ithelp.ithome.com.tw/articles/10242295)

### Swashbuckle 三個主要元件
1. Swashbuckle.AspNetCore.Swagger
1. Swashbuckle.AspNetCore.SwaggerGen
1. Swashbuckle.AspNetCore.SwaggerUI    
    
### 安裝 SwashBuckle + 註冊 swagger
- 可以直接看官方文件，蠻好懂的 >> [[微軟] 開始使用 swashbuckle](https://learn.microsoft.com/zh-tw/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-8.0&tabs=visual-studio)
- 基本上在建專案時有勾選到 OpenAPI，該有的套件、文件都已安裝處理好，主要是要自定義文件細節、數據等操作而已～

### XML 註解
> [官方文件 - XML 註解](https://learn.microsoft.com/zh-tw/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-8.0&tabs=visual-studio-mac#xml-comments)
#### 啟用 XML 註解
> 啟用 XML 註解，可提供未記載之公用類型與成員的偵錯資訊。 
> 可以透過專案設定檔去忽略特定警告碼。也可以在特定類別上用 `pragma` 去隱藏。
    
1. `.csproj` 檔案
    - 可從專案資料夾按右鍵，編輯專案檔，會出現 `.csproj` 檔案
    - 將下面這行放入 `<PropertyGroup>` 內
        ```csharp
        <GenerateDocumentationFile>true</GenerateDocumentationFile>
        ```
2. 產生 XML 文件 (<font color="red">預設</font>)
    - 從 `專案` 進去，選擇 XXX(專案名)屬性
    - 選擇 `編譯器`，勾選 產生 XML 文件

#### api 路徑順序
- 在 program.cs 內設定( 加上這段 )：
    ```csharp
    builder.Services.AddSwaggerGen(options =>
        // 自訂排序，順序由小至大
        options.OrderActionsBy(apiDesc =>
        {
            if (apiDesc.ActionDescriptor.RouteValues["controller"] == "Users")
            {
                return "0"; 
            }
            else if (apiDesc.ActionDescriptor.RouteValues["controller"] == "Todos")
            {
                return "1"; 
            }
            return "2"; // 如後續有其他路徑
        });
    )
    ```
---
#### 在 `program.cs` 配置 JWT 身份驗證服務

```csharp
builder.Services.AddAuthentication(cfg => {
    cfg.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    cfg.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    cfg.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x => {
    x.RequireHttpsMetadata = false;
    x.SaveToken = false;
    x.TokenValidationParameters = new TokenValidationParameters {
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(
            Encoding.UTF8
            .GetBytes(configuration["ApplicationSettings:JWT_Secret"])
        ),
        ValidateIssuer = false,
        ValidateAudience = false,
        ClockSkew = TimeSpan.Zero
    };
});
```
-     
---
## 遇到的問題
### Identity
錯誤訊息：
`The entity type 'IdentityUserLogin<string>' requires a primary key to be defined. If you intend.`
想法：
認為我有在模型類別中提供 [key]Id
問 GPT:
1. TodosContext 要去繼承 IdentityDbContext<Users>
2. Users 類別要去繼承 IdentityUser
但兩個都有寫好，卻一直報錯，無法重新生成 migration
解決：因為繼承自 IdentityUser ，所以會自動生成 string Id，不需額外定義 Id 也不用給[Key]屬性

後來又出現，但把 id 移除也沒效果
解決方法：改在 context檔案內 on model creating 方法內設定以基類去做處理才能 Migration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder); 
    
    
### Identity 處理問題

因為希望可以依照上述users路由去設置功能，所以一開始就定好登入時以信箱去判斷是否已有信箱號碼，如果已有信箱號碼，則去驗證密碼是否正確，如果沒有這個信箱在資料庫內，則創建一個新帳號
後來考量到 Identity 功能可以實現密碼雜湊、身份驗證以及完整的資料關聯功能，所以想加入 Identity 相關功能做使用，但因為其本身的密碼驗證過於複雜(這部分可以透過 program.cs 設定去關掉)，以及 Identity 是以用戶名(UserName)去做主要身份認證，也就是 UserName 不能重複也不能為空
這會造成無法透過同一路徑去實現登入及註冊功能，因為如果將用戶名設為信箱，則會在用戶需再次登入時得到用戶名重複的錯誤訊息
測試及看文件、參考資料花了一段時間後，暫且還是找不到可以同時滿足的解法，最後只能換個角度處理問題，想到以下兩個處理方法，最後選擇先以第二個方法去處理
    1. 新增註冊路徑，就可以在註冊時處理 UserName，登入時就只需信箱密碼處理即可
    2. 不使用 Identity 功能，手動處理密碼雜湊、資料庫關聯的功能
    
    
### 資料庫存取順序問題
    
需要先存 使用者 再存 todo 
但抓不到當前使用者 ID
~~原本要去解析 jwt 的 claimtype >> userid
但 claimtype 是字串(不是這個問題)~~

但不管怎麼存都無法成功，就是說存取順序問題
    
    