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 是字串(不是這個問題)~~
但不管怎麼存都無法成功,就是說存取順序問題