Try   HackMD

C# ASP.NET Core Web API

參考系列影片:[凱哥寫程式] ASP.NET CORE WEB API 入門教學
實作參考:[Day21] C# MVC RESTful API (下) 實作RESTful API - C#&AspNetCore
實作參考 + 完整資訊補充:菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
Swagger 相關:ASP.NET Core Web API 入門心得 - 改善 Enum 註解

建立專案

for macOS
如果是 Visual Studio for mac ,安裝時 .NET 預設版本如果沒有8.0可選,請自行安裝 .NET 8.0,最新版本的SDK有較完整功能可使用

Note

  • 如果是以 Visual Studio for Mac 開啟專案的,在建立專案時記得將 HTTPS Configuration / 針對 HTTPS 進行設定checkbox 移除,否則執行時會遇到無法設定 https 憑證的問題
  • 參考頁面:Opt-out of HTTPS/HSTS on project creation

按下 執行 按鈕後,如果有跳出瀏覽器畫面,且上面有預設的 json 格式資料,就算安裝成功了。

創建第一支 HelloWorld API

  1. 打開 Controllers 資料夾,裡面會看到預設的 controller.cs 檔案。
  2. 在資料夾的地方按右鍵,選 新增 > 新增檔案 > ASP.NET CORE > WEB API 控制器更改檔案名稱HelloWorldController(.cs),按下建立
    • 檔案名稱就算沒有保留 Controller ,程式也可以執行,但這樣命名比較清楚。
    • for mac 版本預設打開時就會先建立好基本 CRUD 的框架。
  3. 裡面會看到 ROUTES,這個就是預設的網址路徑: http://localhost:5154/api/helloworld
    ​​​​[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。
    • 網址不分大小寫
  4. 將原本 [HttpGet] 預設的回傳值 Value1 Value2 改為 Hello World
    ​​​​// GET: api/values
    ​​​​[HttpGet]
    ​​​​public IEnumerable<string> Get()
    ​​​​{
    ​​​​    return new string[] { "Hello", "World" };
    ​​​​}
    
  5. 再回到瀏覽器重整頁面: http://localhost:5154/api/helloworld 就會看到 Hello World 了
    • windows 版本有熱重載,但 mac 似乎沒有(?),所以檔案儲存後,還是需要重新執行才能看到頁面的變動。

開發工具

開發環境介紹

  1. 方案 vs 專案
    • 一個方案底下可以有多個專案和類別庫
    • 每個專案都可以是一個 API
    • 類別庫:舉例->公司本身常用的類別庫就可以放入
  2. 專案檔
    • 在專案資料夾按右鍵,點選編輯專案檔,會出現 XXX.csproj
      ​​​​​​​​<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
  3. 設定檔
    • Properties/launchSettings.json
      ​​​​​​​​  "profiles": {
      ​​​​​​​​    "API": {
      ​​​​​​​​      "commandName": "Project",
      ​​​​​​​​      "launchBrowser": true,
      ​​​​​​​​      "launchUrl": "weatherforecast",
      ​​​​​​​​      "applicationUrl": "http://localhost:5154",
      ​​​​​​​​      "environmentVariables": {
      ​​​​​​​​        "ASPNETCORE_ENVIRONMENT": "Development"
      ​​​​​​​​      },
      ​​​​​​​​      "dotnetRunMessages": true
      ​​​​​​​​    },
      
      • 裡面有預設啟動的 port ( applicationUrl ) / launchUrl,都可以在這裡修改
      • 除了有預設 http 啟動方式,也有 iis 的設定

      Visual Studio for Mac 不支援 IIS Express,這是因為 IIS Express 是 Windows 環境中的一個工具,主要用於本地開發和測試 ASP.NET 應用程序。由於 Mac OS 不支援 IIS,因此在 Visual Studio for Mac 中無法使用 IIS Express。

  4. appsettings.json
    • 算是一個共用的字典,常用到的東西寫在這邊,其他程式需要用到再來這裡抓
    • 有兩個檔案:
      • 部署後 appsettings.json
      • 開發時 appsettings.Development.json
    • 開發環境下,會去讀取 appsettings.Development.json 的值去覆蓋 appsettings.json 的設定
  5. Program.cs
    • 算是設定檔,有需要用到的東西就在這裡載入
    • 註冊各種服務、Swagger 文件的設定 + UI 的調整
  6. Weatherforcast.cs
    • 範例檔的 class 檔案,後續用不到就可移除

POSTMAN - API 測試工具

參考:
POST MAN 安裝+基本操作

  • GET
  • POST

LINQ

  • LINQ = Language Integreted Query 語言整合查詢
  • 使用 LINQ 可以查詢各種不同對象,可以減少學習成本並提升方便性。
    • 可用來查詢 SQL / XML / Object
  • LINQ 有兩種寫法:
    1. 宣告式 Declarative:
      ​​​​​​​​var result = from a in newslist
      ​​​​​​​​             where a.Title == "今天天氣很好"
      ​​​​​​​​             select a;
      
    2. 編程式 Imperative:
      ​​​​​​​​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

    • Microsoft.EntityFrameworkCore.SqlServer (sql server 資料庫,我先替換為 Postgresql)
    • Microsoft.EntityFrameworkCore.InMemory (暫存資料庫)
    • Npgsql.EntityFrameworkCore.PostgreSQL (postgresql 資料庫)
    • Microsoft.EntityFrameworkCore.Tools (下指令時會使用到這個套件)

使用 Database First

參考:Entity Framework Core資料庫連線,使用Database First

  • 有提到 依賴注入
  • 資料庫設定(以 postgres 為例):

    • 先到 PGadmin 建立伺服器、資料庫、表格
    • Server 名稱隨意
    • Database / User / Password 這幾個要記得
    • Host 先設為 localhost 或 127.0.0.1
    • 可以先建立三筆資料(後續測試連線用)
  • Scaffold-DbContext

    資料庫變動

    後續只要資料庫有變動,重新執行一次 dbcontext scaffold 完整指令即可

    1. 在 package manager console 上執行
      ​​​​​​​​$ Scaffold-DbContext "Host=localhost;Database=YourDatabase;Username=YourUsername;Password=YourPassword" Npgsql.EntityFrameworkCore.PostgreSQL -OutputDir Models
      
    2. 在終端機上執行
      ​​​​​​​​$ 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 才需要這個寫法)。因為改以依賴注入處理,所以這段可以省略。如果沒加上這個參數,會自動產生以下程式碼:
        ​​​​​​​​​​​​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

    ​​​​"ConnectionStrings": {
    ​​​​    "DefaultConnection": "Host=localhost;Database=postgres;Username=postgres;Password=your_password"
    ​​}
    
  • DI 注入,寫入 Program.cs

    ​​​​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
    ​​​​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] 如果套用在組件時,組件中所有控制器都會套用這個屬性。

      官方文件:組件上的屬性
      在 .NET 中,組件是一個編譯後的單位,通常是 .dll 或 .exe 檔案。它是由 C# 檔案編譯而成的,並且可以包含類、接口、資源等。

    • private readonly postgresContext _postgresContext; 第一個 postgresContext 是資料庫名稱,第二個_postgresContext 是變數名稱,習慣加上底線
    • Constructor 建構類別 (和 class 同名且類型為public),當在 program.cs 內有寫到依賴注入設定時,在這裏就是去取得依賴注入物件
  • 按下啟動按鈕,在瀏覽器上輸入路徑,確認前面輸入的資料是否成功顯示

使用 Code First

  • 先建立 Models 資料夾
  • 建立資料表(table)
    • 新增 > 新增檔案 > 一般 > 類別是空的
    • 檔名設為 News (以下 table 以 News 為例)
  • 建立資料庫物件
    • 新增 > 新增檔案 > 一般 > 類別是空的
    • 檔名設為 webContext (資料庫名稱+Context,取名慣例)
  • 資料庫物件寫法:
    ​​​​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
    ​​"ConnectionStrings": {
    ​​​​    "DefaultConnection": "Server=localhost;Database=web;User Id=your_username;Password=your_password;TrustServerCertificate=True;"
    ​​}
    
    • 這邊要注意 不同資料庫的連線字串 需要的欄位名稱不同
  • DI 注入,寫入 Program.cs
    ​​​​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(); 之前
    • 一定要放在 builder.Build 指令前,這個指令是在創建一個 app 實例,所以所有服務都要在此之前註冊完成
  • 生成描述檔
    • $ dotnet ef migrations add InitialCreate
    • InitialCreate 是描述檔名稱
  • 更新資料庫
    • $ dotnet ef database update
    • 會產生兩個資料表,其中一個是紀錄更新變化的資料表
  • 如果要指定某個資料表,可用參數 --context webContext

初始資料建立 (假資料) SeedData

  • 在 Models 資料夾內建立 SeedData.cs 檔案
    ​​​​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
    ​​​​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>

    ​​​​[HttpGet]
    ​​​​public IEnumerable<Post> Get()
    ​​​​{
    ​​​​    return _postgresContext.Posts;
    ​​​​        // 資料庫物件.資料表名稱
    ​​​​}
    
  2. IActionResult 寫法 *

    • 可以靈活去設定返回的響應內容和狀態碼
    ​​​​[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);
    ​​​​}
    

只顯示部分欄位

  • 只想顯示部分資料給使用者
  • 節省記憶體容量
  • 宣告式寫法:
    ​​​​[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 資料傳輸物件
參考:建立資料傳輸物件

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

建立 DTO 檔案

  1. 建立 Dtos 資料夾
    • 後續可能還會有其他 DTO 檔案
  2. 建立 PostDto.cs 檔案
    ​​​​namespace API.Dtos
    ​​​​{
    ​​​​    public class PostDto
    ​​​​    {
    ​​​​        public int Id { get; set; }
    ​​​​        public string Title { get; set; }
    ​​​​        public string Type { get; set; }
    ​​​​    }
    ​​​​}
    

    { get; set; }

    • get/set 訪問器,分別用於 讀取屬性的值設置屬性的值
    • set 訪問器可以添加驗證邏輯。
    • 自動實現的屬性{ get; set; },無需顯式定義私有字段。
    • 使用 private setreadonly{ get; private set; } ,或 readonly 只使用 get 訪問器而不使用 set 訪問器,屬性在物件創建後就不會被更改。
    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;
               }
           }
       }
    
  3. PostController.cs 檔案
    ​​​​[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 寫法
    ​​​​// 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;
    ​​​​}
    
    1. IActionResult 寫法 *
    ​​​​[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] 是指請求主體
    ​​​​[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
    ​​​​public void Post([FromBody] string value){}
    

Postman 測試

GET

POST

  • Body 新增內文
    ​​​​{
    ​​​​    "Title": "看電影",
    ​​​​    "IsCompleted": false
    ​​​​}
    
  • 如果一開始得到 415 ,可能是沒有設置標頭的關係
    • Headers 新增 Key: Content-Type / Value: application/json

User 登入登出

1. 建立模型類別

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 類別

public class UserDto
{
    public string Email { get; set; }
    public string Password { get; set; }
}
public class TokenDto
{
    public string Token { get; set; }
    public DateTime Expiration { get; set; }
}

3. 建立用戶服務(Service)


  • Task / Task<T>

    Task 是一種型別,用來表示一個異步操作的結果,通常用於表示一個正在進行的操作的狀態,並在操作完成時返回結果。
    • Task 表示一個不返回結果的異步操作。
    • Task<T> 表示一個返回結果的異步操作,其中 T 為返回的類型。

4. 建立控制器(Controller)


JWT 驗證

JWT = Json Web Token

參考:

  • JWT 組成:
    1. HEADER
    2. PAYLOAD
    3. VERIFY SIGNATURE

JWT 設定參數

  • appsettings.cs 裡面放上 JWT 設定參數
    ​​​​"JwtSettings": {
    ​​​​    "Issuer": "TestAPI",
    ​​​​    "SignKey": "1qaz@WSX",
    ​​​​    "ExpireSec": 30000
    ​​​​}
    
    1. Issuer 發行人
    2. SignKey 用來加密的金鑰字串
    3. ExpireSec 有效期限

    如何讀取到 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 文件

Swashbuckle 三個主要元件

  1. Swashbuckle.AspNetCore.Swagger
  2. Swashbuckle.AspNetCore.SwaggerGen
  3. Swashbuckle.AspNetCore.SwaggerUI

安裝 SwashBuckle + 註冊 swagger

  • 可以直接看官方文件,蠻好懂的 >> [微軟] 開始使用 swashbuckle
  • 基本上在建專案時有勾選到 OpenAPI,該有的套件、文件都已安裝處理好,主要是要自定義文件細節、數據等操作而已~

XML 註解

官方文件 - XML 註解

啟用 XML 註解

啟用 XML 註解,可提供未記載之公用類型與成員的偵錯資訊。
可以透過專案設定檔去忽略特定警告碼。也可以在特定類別上用 pragma 去隱藏。

  1. .csproj 檔案
    • 可從專案資料夾按右鍵,編輯專案檔,會出現 .csproj 檔案
    • 將下面這行放入 <PropertyGroup>
      ​​​​​​​​<GenerateDocumentationFile>true</GenerateDocumentationFile>
      
  2. 產生 XML 文件 (預設)
    • 專案 進去,選擇 XXX(專案名)屬性
    • 選擇 編譯器,勾選 產生 XML 文件

api 路徑順序

  • 在 program.cs 內設定( 加上這段 ):
    ​​​​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 身份驗證服務

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 是字串(不是這個問題)

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