11

ASP.NET Core 極簡風 - Minimal API

 2 years ago
source link: https://blog.darkthread.net/blog/minimal-api/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Minimal API-黑暗執行緒

先前展示過用 50 行 Program.cs 寫個 ASP.NET Core CORS 上傳服務,從讀者 Joker 留言我學到新名詞 - Minimal API,身為極簡主義者,它絕對是我的菜,特整理一篇備忘。

對於 Minimal API,官方文件有篇完整介紹 - Minimal APIs overview,這篇是我自己的摘要與整理,方便未來施工參考。

首先,ASP.NET Core 從 6.0 開始引進 Minimal API,配合 Top-Level Statements,當使用 dotnet new web 建立一個空白網站時,整個專案程式部分只有一個 Program.cs,而且只有四行:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

但這樣就夠跑出一個 Hello World! 超迷你網站了。

dotnet new web 所建立的空白網站專案使用 WebApplicationBuilder 建立網站,它會自動依 Properties/launchSettings.json 設定同時開放 http: 及 https: 存取。我們也可透過 app.Run("http://localhost:3000")app.Urls.Add("http://localhost:3000") 指定 URL,但實務上通常不會寫死在程式裡,透過 dotnet run --urls="http://lcoalhost:3000" 或指定 ASPNETCORE_URLS=http://localhost:3000 環境變數,或是由 appsettings.json 設定檔指定是較常見的做法。

SSL 憑證

當網站啟用 HTTPS 時,ASP.NET Core 預設會產生並使月開發測試專用自簽憑證:

若要換成其他憑證,可修改 appsettings.json 指定憑證及私鑰檔案(註:用 OpenSSL 自建 CA 及發憑證方法可參考:使用 OpenSSL 製作 SSL 憑證):(順便示範用 --urls 參數指定網端傾聽網址)

如此,ASP.NET Core 網站即會改用指定的憑證。

DI 與服務

另外,ASP.NET Core 6.0 省略 Startup.cs 又大幅簡化了 Program.cs,不再有 public void ConfigureServices(IServiceCollection services)、public void Configure(IApplicationBuilder app, IWebHostEnvironment env),不就無法用依賴注入存取 IConfiguration、IWebHostEnvironment、ILoggerFactory ?(延伸閱讀:不可不知的 ASP.NET Core 依賴注入

一開始接觸 ASP.NET Core 6 我有點不知所措,後來知道 ASP.NET Core 把它們都掛在 WebApplication 物件下:

  • app.Configuration --> IConfiguration
  • app.Environment --> IWebHostEnvironment
  • app.Logger --> ILogger

至於原本寫在 ConfigureServices(IServiceCollection services) 的服務註冊,則要改用 builder.Services,例如:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

除了基本功能,WebApplication 也可以加掛各式各樣的 Middleware,像是 app.UseFileServer() 後,網站便能提供 .html、.css、.js 及圖檔存取,另外像是帳號登入、CORS、Response Cache、WebSocket... 等,都可視需要加掛。

直接用範例說明常用的請求處理規則、路由設定及參數取得:

// 同一網址依 HTTP Method 決定處理邏輯
app.MapGet("/", () => "This is a GET"); 
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

// 可一次對映多個 HTTP Method
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");
// 由 URL 路徑取得參數
app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
    
// 萬用字元 * 將後方部分視為一個參數
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

// 加上參數型別限制
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

除了透過路由取值,ASP.NET Core 也會自動繫結從 QueryString、Header、Body(JSON 格式)、DI 送來的參數(注意:.NET 6 未內建支援繫結 Form 送出內容),例如:

// id 來自 URL 路徑、page 來自 QueryString 或 Header,service 則來自依賴注入
app.MapGet("/{id}", (int id, int page, ISomeService service) => { });
// person 來自 Body JSON
app.MapPost("/", (Person person) => { });
// 明確指定參數來源
app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});
// 參數預設為必要,選擇性參數需額外註明
app.MapGet("/list1", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.MapGet("/list2", (int pageNumber = 1) => $"Requesting page {pageNumber}");

若繫結邏輯複雜,還有兩種客製化繫結的方法:1. 型別提供 TryParse() API (可用擴充方法為現有型別加上 TryParse()) 2. 自訂 BindAsync() API。

以上範例都是傳回字串作為網頁回應,若程式邏輯較複雜需要操作 HttpRequest、HttpResponse,語法如下:

app.MapGet("/a", (HttpContext context) => context.Response.WriteAsync("Hello World"));
app.MapGet("/b", (HttpRequest request, HttpResponse response) =>
    response.WriteAsync($"Hello World {request.Query["name"]}"));
app.MapGet("/c", async (CancellationToken cancellationToken) => 
    await MakeLongRunningRequestAsync(cancellationToken));
app.MapGet("/d", (ClaimsPrincipal user) => user.Identity.Name);

ASP.NET Core 提供了幾種回應形式:

// 字串內容
app.MapGet("/hello", () => "Hello World"); 
// 傳回JSON
app.MapGet("/hello", () => new { Message = "Hello World" }); 
// 傳回 IResult
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
// 指定 StatusCode
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK) // 若為 Todo 型別傳 200
   .Produces(StatusCodes.Status404NotFound);
// 傳回 JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
// 自訂 Status Code
app.MapGet("/405", () => Results.StatusCode(405));
// 傳純文字
app.MapGet("/text", () => Results.Text("This is some text"));
// 傳回 Stream
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://consoto/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});
// 重新導向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
// 傳回檔案
app.MapGet("/download", () => Results.File("myfile.text"));

若 ASP.NET Core 提供的回應形式不敷使用,也可以自己客製 IResult

還有一些較進階的應用情境,像是身分認證、CORS、OpenAPI... 這部分我應用 Miminal API 的情境不太會遇到,記下關鍵字,等遇到再學。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK