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