2024年8月8日 星期四

使用Scaffolding功能建立Web API - 1

 .NET Magazine國際中文電子雜誌

作 者:許薰尹
審 稿:張智凱
文章編號:N240726302
出刊日期: 2024/7/24

這篇文章將介紹Visual Studio 2022 開發ASP.NET Core Web API提供的一些功能,例如*.http檔案、Swagger、Scaffolding功能與Endpoint explorer瀏覽與測試Web API。



建立「ASP.NET Core Web API」專案


首先使用Visual Studio 2022開發工具建立一個「ASP.NET Core Web API」專案,建立步驟如下:啟動Visual Studio 2022開發環境,從「開始」視窗選取「Create a new project」選項。




圖 1:建立「ASP.NET Core Web API」專案。


從Visual Studio 2022開發工具的「Create a new project」對話盒中,選取 使用C# 語法的「ASP.NET Core Web API」項目,然後按一下「Next」按鈕,請參考下圖所示:




圖 2:「ASP.NET Core Web API」項目。



下一步,在「Configure your new project」視窗中,設定ASP.NET Core Web API專案名稱與專案存放路徑,然後按下「Next」按鈕,請參考下圖所示:




圖 3:設定ASP.NET Core Web API專案名稱與專案存放路徑。


下一步,在「Additional information」視窗中,設定「Target Framework」為「NET 8.0 (Long Term Support)」;選擇「Configure for HTTPS」與「Enable OpenAI Support」,然後按下「Create」按鈕,請參考下圖所示:



圖 4:設定「Target Framework」為「NET 8.0 (Long Term Support)」。


當專案建立完成之後,檢視「Program.cs」檔案,包含以下範例程式碼,其中有一個「weatherforecast」服務可以取得天氣狀況,並支援「Swagger」以產生文件和服務測試頁面:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var builder = WebApplication.CreateBuilder( args );
 
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer( );
builder.Services.AddSwaggerGen( );
 
var app = builder.Build( );
 
// Configure the HTTP request pipeline.
if ( app.Environment.IsDevelopment( ) ) {
  app.UseSwagger( );
  app.UseSwaggerUI( );
}
 
app.UseHttpsRedirection( );
 
var summaries = new[ ]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
 
app.MapGet( "/weatherforecast", () => {
  var forecast = Enumerable.Range( 1, 5 ).Select( index =>
      new WeatherForecast
      (
          DateOnly.FromDateTime( DateTime.Now.AddDays( index ) ),
          Random.Shared.Next( -20, 55 ),
          summaries[ Random.Shared.Next( summaries.Length ) ]
      ) )
      .ToArray( );
  return forecast;
} )
.WithName( "GetWeatherForecast" )
.WithOpenApi( );
 
app.Run( );
 
internal record WeatherForecast( DateOnly Date, int TemperatureC, string? Summary ) {
  public int TemperatureF => 32 + (int) ( TemperatureC / 0.5556 );
}

 


專案根目錄中也包含一個「專案名稱.http」檔案,例如本文範例的檔案名稱為「MyAPIDemo.http」,參考程式碼如下,它是一種在IDE或支援REST客戶端功能的程式開發工具中,使用來送出HTTP請求的檔案。這類檔案允許開發者直接從他們的程式開發工具定義和發送HTTP請求,這樣可以方便地測試Web API,無需使用類似「Postman」這樣的應用程式,或在終端中使用「curl」命令送出請求。

1
2
3
4
5
6
@MyAPIDemo_HostAddress = http://localhost:5260
 
GET {{MyAPIDemo_HostAddress}}/weatherforecast/
Accept: application/json
 
###

 

 



我們需先執行網站,才能透過「MyAPIDemo.http」檔案來測試Web API。在Visual Studio 2022開發工具,按CTRL+F5執行網站,然後點選「MyAPIDemo.http」檔案中「GET」這行程式上方的「Send request」連結,就會自動送出HTTP GET請求,視窗右方將會顯示請求執行的結果,請參考下圖所示:



圖 5:使用「專案名稱.http」檔案送出HTTP GET請求。


若點選執行結果畫面的「Raw」連結,可以檢視HTTP回應的原始資料格式,請參考下圖所示:



圖 6:HTTP回應的原始資料格式。


點選執行結果畫面的「Headers」連結,可以檢視HTTP回應的表頭資訊,請參考下圖所示:




圖 7:HTTP回應的表頭資訊。



點選執行結果畫面的「Request」連結,可以檢視HTTP請求的資訊,如表頭、內文等,請參考下圖所示:




圖 8:HTTP請求的資訊。



加入模型類別


預設Visual Studio 2022提供Scaffolding功能,我們先加入一個模型類別描述圖書資料。從「Solution Explorer」視窗專案名稱資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」>「Class」項目,選取「Class」,將名稱設定為「Book」,然後按下「Add 」按鈕,請參考下圖所示:




圖 9:加入「Book」類別。



在「Book」類別之中加入以下屬性,描述圖書資料:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System.ComponentModel.DataAnnotations;
 
namespace MyAPIDemo {
 
  public class Book {
    [Display( Name = "圖書編號" )]
    public int Id { get; set; }
 
    [Display( Name = "圖書名稱" )]
    [Required( ErrorMessage = "圖書名稱不可為空白" )]
    [MaxLength( 50, ErrorMessage = "長度不可超過 {1}" )]
    public string? Title { get; set; } = null!;
 
    [Display( Name = "價格" )]
    [Range( 1, int.MaxValue, ErrorMessage = "{0} 有效範圍在 {1} 與 {2} 之間" )]
    public int Price { get; set; }
 
    [Display( Name = "出版日期" )]
    [DataType( DataType.Date )]
    public DateTime PublishDate { get; set; }
 
    [Display( Name = "庫存" )]
    public bool InStock { get; set; }
 
    [Display( Name = "說明" )]
    [MaxLength( 50, ErrorMessage = "長度不可超過 {1}" )]
    public string? Description { get; set; }
 
 
  }
 
}

 



選取Visual Stuido 「Build」>「Build Solution」項目編譯程式碼,確定專案程式無誤,因為接下來的動作需要專案正確編譯才能進行。


使用Scaffold功能產生程式碼


接著透過Visual Studio Scaffold功能產生使用Entity Framework Core 存取資料庫的Web API程式��,從「Solution Explorer」視窗專案名稱資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」>「New Scaffolded Item」項目,請參考下圖所示:




圖 10:使用Scaffold功能產生程式碼。


在「Add New Scaffolded Item」對話盒中選取「API with read / write endpoints , using Entity Framework」項目,然後按下「Add 」按鈕,請參考下圖所示:



圖 11:選取「API with read / write endpoints , using Entity Framework」項目。


在「Add API with read / write endpoints , using Entity Framework」視窗,從「Model class」右方的下拉式清單方塊中選取「Book」;點選「Endpoints class」右方的「+」按鈕,請參考下圖所示:



圖 12:加入Endpoints class。


在「Add Endpoints Class」視窗中輸入名稱,然後按「Add」按鈕,請參考下圖所示:



圖 13:加入Endpoints class。


接下來會回到「Add API with read / write endpoints , using Entity Framework」視窗,點選「DbContext class」右方的「+」按鈕,請參考下圖所示:



圖 14:加入「DbContext」類別。


在「Add Data Context」視窗中輸入名稱,然後按「Add」按鈕,請參考下圖所示:




圖 15:加入「DbContext」類別。



接下來會回到「Add API with read / write endpoints , using Entity Framework」視窗,點選「Database provider」右方的下拉式清單方塊,選取「SQL Server」,然後按下「Add」按鈕,請參考下圖所示:




圖 16:選取「Database provider」。


接著Visual Studio會在專案中加入一個「BookEndpoints.cs」類別,此類別用於集中定義和對應操作圖書資料相關的 HTTP 端點,其中的「MapBookEndpoints」方法擴充了「IEndpointRouteBuilder」介面,允許你直接在方法內配置路由,程式參考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.OpenApi;
using MyAPIDemo.Data;
namespace MyAPIDemo;
 
public static class BookEndpoints
{
    public static void MapBookEndpoints (this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/Book").WithTags(nameof(Book));
 
        group.MapGet("/", async (MyAPIDemoContext db) =>
        {
            return await db.Book.ToListAsync();
        })
        .WithName("GetAllBooks")
        .WithOpenApi();
 
        group.MapGet("/{id}", async Task<Results<Ok<Book>, NotFound>> (int id, MyAPIDemoContext db) =>
        {
            return await db.Book.AsNoTracking()
                .FirstOrDefaultAsync(model => model.Id == id)
                is Book model
                    ? TypedResults.Ok(model)
                    : TypedResults.NotFound();
        })
        .WithName("GetBookById")
        .WithOpenApi();
 
        group.MapPut("/{id}", async Task<Results<Ok, NotFound>> (int id, Book book, MyAPIDemoContext db) =>
        {
            var affected = await db.Book
                .Where(model => model.Id == id)
                .ExecuteUpdateAsync(setters => setters
                    .SetProperty(m => m.Id, book.Id)
                    .SetProperty(m => m.Title, book.Title)
                    .SetProperty(m => m.Price, book.Price)
                    .SetProperty(m => m.PublishDate, book.PublishDate)
                    .SetProperty(m => m.InStock, book.InStock)
                    .SetProperty(m => m.Description, book.Description)
                    );
            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
        })
        .WithName("UpdateBook")
        .WithOpenApi();
 
        group.MapPost("/", async (Book book, MyAPIDemoContext db) =>
        {
            db.Book.Add(book);
            await db.SaveChangesAsync();
            return TypedResults.Created($"/api/Book/{book.Id}",book);
        })
        .WithName("CreateBook")
        .WithOpenApi();
 
        group.MapDelete("/{id}", async Task<Results<Ok, NotFound>> (int id, MyAPIDemoContext db) =>
        {
            var affected = await db.Book
                .Where(model => model.Id == id)
                .ExecuteDeleteAsync();
            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
        })
        .WithName("DeleteBook")
        .WithOpenApi();
    }
}



以下分別說明「BookEndpoints」靜態類別的「MapBookEndpoints」靜態方法中的程式碼。首先,程式碼建立了一個「group」變數,使用「routes.MapGroup」方法將API端點對應到「/api/Book」路徑,並指定了「Book」作為標籤:

1
var group = routes.MapGroup( "/api/Book" ).WithTags( nameof(Book) );


接下來,程式碼使用「group.MapGet」方法對應了一個GET請求的端點,該端點的路徑為「/」。這個端點使用了一個非同步的委派,接受一個「MyAPIDemoContext」物件作為參數,並從資料庫中取得所有的書籍資料。最後,將書籍資料以「List」的形式回傳。

1
2
3
4
5
6
group.MapGet("/", async (MyAPIDemoContext db) =>
{
    return await db.Book.ToListAsync();
})
.WithName("GetAllBooks")
.WithOpenApi();


接著使用「group.MapGet」方法對應了另一個GET請求的端點,該端點的路徑為「/{id}」,其中「id」是一個整數參數。這個端點也使用了一個非同步的委派,接受一個整數型別的「id」與「MyAPIDemoContext」物件作為參數。在這個端點中,從資料庫中查詢「id」相符的書籍資料,並將結果回傳。如果找到了書籍資料,則叫用「Ok」方法並將書籍資料作為內容回傳;如果找不到書籍資料,則回傳「NotFound」。

1
2
3
4
5
6
7
8
9
10
group.MapGet("/{id}", async Task<Results<Ok<Book>, NotFound>> (int id, MyAPIDemoContext db) =>
{
    return await db.Book.AsNoTracking()
        .FirstOrDefaultAsync(model => model.Id == id)
        is Book model
            ? TypedResults.Ok(model)
            : TypedResults.NotFound();
})
.WithName("GetBookById")
.WithOpenApi();


然後使用「group.MapPut」方法對應了一個PUT請求的端點,該端點的路徑為「/{id}」,其中「id」是一個整數參數。這個端點也使用了一個非同步的委派,接受一個整數型別的「id」、一個「Book」物件與「MyAPIDemoContext」物件作為參數。在這個端點中,程式碼根據指定的「id」在資料庫中更新對應的書籍資料。如果更新成功,叫用「Ok」方法並將書籍資料作為內容回傳;如果找不到書籍資料,則回傳「NotFound」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
group.MapPut( "/{id}", async Task<Results<Ok, NotFound>> ( int id, Book book, MyAPIDemoContext db ) => {
  var affected = await db.Book
      .Where( model => model.Id == id )
      .ExecuteUpdateAsync( setters => setters
          .SetProperty( m => m.Id, book.Id )
          .SetProperty( m => m.Title, book.Title )
          .SetProperty( m => m.Price, book.Price )
          .SetProperty( m => m.PublishDate, book.PublishDate )
          .SetProperty( m => m.InStock, book.InStock )
          .SetProperty( m => m.Description, book.Description )
          );
  return affected == 1 ? TypedResults.Ok( ) : TypedResults.NotFound( );
} )
.WithName( "UpdateBook" )
.WithOpenApi( );


接著,程式碼使用「group.MapPost」方法對應一個POST請求的端點,該端點的路徑為「/」。這個端點也使用了一個非同步的委派,接受一個「Book」物件和一個「MyAPIDemoContext」物件作為參數。在這個端點中,將接收到的書籍資料新增到資料庫中,並叫用「Created」方法回傳結果,其中包含了新增書籍的路徑和書籍的內容:

1
2
3
4
5
6
7
8
group.MapPost("/", async (Book book, MyAPIDemoContext db) =>
    {
        db.Book.Add(book);
        await db.SaveChangesAsync();
        return TypedResults.Created($"/api/Book/{book.Id}",book);
    })
    .WithName("CreateBook")
    .WithOpenApi();


最後,程式碼使用「group.MapDelete」方法對應了一個「DELETE」請求的端點,該端點的路徑為「/{id}」,其中「id」是一個整數參數。這個端點也使用了一個非同步的委派,接受一個整數型別的「id」與一個「MyAPIDemoContext」物件作為參數。在這個端點中,程式碼根據指定的「id」從資料庫中刪除對應的書籍資料。如果刪除成功,則叫用「Ok」方法回傳結果;如果找不到對應的書籍資料,則叫用「NotFound」方法回傳結果。

1
2
3
4
5
6
7
8
9
group.MapDelete("/{id}", async Task<Results<Ok, NotFound>> (int id, MyAPIDemoContext db) =>
{
    var affected = await db.Book
        .Where(model => model.Id == id)
        .ExecuteDeleteAsync();
    return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
})
.WithName("DeleteBook")
.WithOpenApi();


同時工具也會在專案中加入一個「MyAPIDemoContext.cs」檔案,參考程式碼如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using MyAPIDemo;
 
namespace MyAPIDemo.Data
{
    public class MyAPIDemoContext : DbContext
    {
        public MyAPIDemoContext (DbContextOptions<MyAPIDemoContext> options)
            : base(options)
        {
        }
 
        public DbSet<MyAPIDemo.Book> Book { get; set; } = default!;
    }
}



「MyAPIDemoContext」類別繼承自「DbContext」類別。「DbContext」是Entity Framework Core中的一個類別,用於處理與資料庫的互動。在「MyAPIDemoContext」類別中包含一個建構函式「MyAPIDemoContext」,它接受一個「DbContextOptions
」型別的參數,並呼叫基底類別的建構函式「base(options)」。這個建構函式用於設定資料庫連線和其他相關的選項。

「MyAPIDemoContext」類別中包含一個「DbSet」屬性,它表示資料庫中的資料表資料合。這個屬性的名稱是「Book」。這個「DbSet」屬性用於對資料庫進行CRUD操作,例如查詢、新增、更新和刪除。

資料庫的連結資訊儲存在專案中的「appsettings.json」檔案之中,參考程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MyAPIDemoContext": "Server=(localdb)\\mssqllocaldb;Database=MyAPIDemoContext-42d5550a-9624-486b-ba91-906be590fb34;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}
 

預設工具使用SQL Server localdb資料庫來存放資料,我們可以根據需求變更資料庫,例如修改「appsettings.json」檔案的內容,改用SQL Server Express來存放資料,參考程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MyAPIDemoContext": "Server=.\\sqlexpress;Database=MyAPIDemoDb;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=true"
  }
}
 



最後工具會在專案中的「Program.cs」檔案中註冊「MyAPIDemoContext」服務,參考程式碼如下,這個方法告訴應用程式使用 SQL Server 資料庫作為資料庫提供者,並從組態檔案讀取名為「MyAPIDemoContext」的連接字串來連接到資料庫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using MyAPIDemo.Data;
using MyAPIDemo;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<MyAPIDemoContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MyAPIDemoContext") ?? throw new InvalidOperationException("Connection string 'MyAPIDemoContext' not found.")));
 
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
 
var app = builder.Build();
 
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
 
app.UseHttpsRedirection();
 
var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
 
app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
 
app.MapBookEndpoints();
 
app.Run();
 
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
 



「Program.cs」檔案最後將一個叫用「MapBookEndpoints」方法來設定Web API應用程式的路由。







0 意見:

張貼留言