【ASP.NET Core】體驗一下 Mini Web API

在上一篇水文中,老周給大夥伴們簡單演示了通過 Socket 編程的方式控制 MPD (在樹莓派上)。按照計劃,老周還想給大夥伴們演示一下使用 Web API 來封裝對 MPD 控制。思路很 Easy,樹莓派上使用本地 Socket 來封裝一下,然後以 Web API 的方式對客戶端公開。這樣有一個好處:之後不管你打算把客戶端做成桌面窗口,還是 Web 頁面,或是做成手機 App,你都可以直接調用這套 Web API。這樣一來,很多程式碼就不必重複寫了,省時省力,減少腦細胞的大量死亡。

如果大家比較關心時事的話,應該知道 .NET 6 的 ASP.NET Core 有一個新特性—— Mini API,或 Mini Web API。有一個不錯的翻譯叫做「極簡 API」。其實,極簡的不只是 Web API,整個 ASP.NET Core 應用項目的結構都精簡了不少。最讓老周高興的就是沒有了 Startup 類(其實早期版本中也可以不使用 Startup,如果你以前看過老周的誤人子弟教程的話,你應該有印象),也不用費心地搞個什麼 ConfigureService 又要弄個 Configure 方法約定了。再配合 C# 9 的新功能,連 Main 方法都省了。所以初始化配置工作都可以在一個程式碼文件中搞定。

使用這個簡化版的 Web API 來做 MPD 的封裝還真的不錯。不過,本文老周先不弄這個,先讓大夥伴們了解一下 Mini API 怎麼玩——權且當作預備知識。

.NET 6 還有個新功能也不錯,就是全局的 using 指令。以前在 C 語言中,如果你寫一個 abc.h 頭文件,裡面放上這些程式碼:

#ifndef _ABC_H_
#define _ABC_H_

#include "nc.h"
#include "nt.h"
#include "zz.h"
#include "xb.h"
#include "sb.h"

#endif

然後在程式碼文件中 include 一個這個 abc.h 就可以間接引用這些頭文件,但 C# 中沒有這種玩法,每個程式碼文件要用到哪些命名空間,都要寫一遍 using 指令。在隨同 .NET 6 一同發布的 C# 10 中終於有全局 using 了。只要你在其中一個程式碼文件中寫上全局 using 指令,然後其他程式碼文件中就不必再 using 了。

方法是在 using 前加上 global 就行了。C# 雖然早有 global 關鍵字,但在過去這個並不是全局 using 用的,而是專用來標識 .NET 框架中的命名空間的,主要是防止命名空間重名的。

 

好了,咱們現在就去嗨一下 Mini API吧。

此處假設你已經安裝好 VS 2022 和 .NET 6。在創建新項目時選擇空白 ASP.NET Core 應用程式。用空白項目方便稍後寫自己的 Code。

 

然後輸入項目名稱以及存放路徑。

 

選擇.net 版本號。

 

  最後,確定新創建項目。

---------------------------------------------------------------------------------

項目創建後你會發現,真TM簡潔了不少,Program.cs 文件中只有這麼幾行。

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

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

app.Run();

第一行:調用 CreateBuilder 方法創建一個builder ,這個 builder 隨後用於構建 Web 應用程式。

第二行:直接就用預設的參數構建了一個 app 對象。

第三行:配置 HTTP 管道——怎麼處理HTTP請求。此處配置表明只向客戶端返回「Hello World!」。

第四行:運行 app。

你,不用再寫 Startup 類了,也不用遵守方法簽約定去寫 ConfigureServices 等方法了。

要配置服務咋辦?直接 Services Add。例如

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddRazorPages();
builder.Services.AddAntiforgery()
                .AddWebEncoders()
                .AddSingleton<XXX, YYY>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");

app.Run();

看看是不是很整齊?注意這個過程是在 build 方法調用之前完成的。原因和以前 Startup 類中寫方法一樣,Add 服務是聲明咱們的應用程式中要使用哪些功能,哪些對象被用於依賴注入,一旦 build 了,程式的功能結構就確定下來了,所以應用程式構建後就不再修改其功能了。

以前在寫 Startup 時,我們知道,還有一個 Configure 方法用來配置中間件的,也就是剛剛說的配置HTTP管道。build 方法構建 app 後,就可以直接通過這個 app 實例來配置你要 Use 的東西。例如

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// 中間件配置
app.MapGet("/", () => "Hello World!");
app.UseRouting();
app.UseCookiePolicy();
app.Use(async (context, next) =>
{
    context.Response.Headers.Add("Server", "Big Bomb");
    await next();
});

 app.MapControllers();
 app.MapBlazorHub();
 app.MapRazorPages();

app.Run();

如果我們要編寫 Mini API,不需要 Add 什麼 Service,也不需要 Use 什麼組件,在 build 和 app.run 之間直接 MapXXX 就行了。XXX 指 HTTP 請求方法,比如 GET、POST、PUT 等。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
//====================================
// Mini API 寫在這裡
//====================================
app.Run();

 

相當有意思的是:這些 MapXXX 擴展方法都有一個 handler 參數,類型是 Delegate。也就是說你在調用時可以傳遞任何類型的委託對象。於是,就可以這樣搞:

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

app.UseHttpsRedirection();
//====================================
// Mini API 寫在這裡
app.MapGet("/greet", () =>
{
    return "What the fuck?";
});
//====================================
app.Run();

一個 Web API 就完成了。真的,可以用了,不信運行下看看。拿出秘密武器——Postman,測試一下。

 

 

用 Postman 還是有些不夠爽,而且這貨現在越做越複雜,還整天叫你註冊帳號,實屬無趣。我們改為用 swagger 來測試。打開 Nuget 包管理器,搜索 swagger。

 

 安裝這個包包。

 

把程式碼改一下。

var builder = WebApplication.CreateBuilder(args);
// 不要忘了加這兩個服務
builder.Services.AddSwaggerGen();
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
//====================================
// Mini API 寫在這裡
app.MapGet("/greet", () =>
{
    return "What the fuck?";
});
//====================================
app.Run();

這樣一來,咱們在開發測試階段就可以直接用這個組件在 Web 頁上測試 API 的調用了,不再啟動其他工具軟體了。

運行該項目,然後瀏覽器定位到 http(s)://root_url:port/swagger。

比如,我運行後得到的URL是 //localhost:7189,那麼在瀏覽器中打開 //localhost:7189/swagger。

 

 

點擊右邊的「try it out」,然後點 「Execute」 按鈕,就能看到執行結果了。

 

 

怎麼樣,好用吧?咱們再添加一個API,這次要帶參數的 POST 請求。

app.MapPost("/submit", (string name, int age) =>
{
    return $"你提交的內容:{name} - {age}";
});

這個 API 帶兩個參數,分別取名為 name,age。在調用時,是通過請求的 body 來提供的,格式為 JSON。

前面老周說過,這些MapXXX方法的委託參數是 Delegate 類型,所以你可以用任意類型的委託,有參數的沒參數的,有返回值的沒返回值的。

每次調試時都要在瀏覽器地址輸入 http(s)://root/swagger 太TM不方便了,咱們可以配置一下,讓其自動定位到 swagger 下。打開項目屬性窗口,轉到「調試」標籤。

 

 點擊頁面上的「打開調試啟動配置文件 UI」鏈接。

 

 把滾動條往地獄方向拉,一直拉到看到「URL」標題,在文本中填上 swagger。搞定。

現在,你直接運行項目,就自動打開 API 列表了。點擊展開 submit,再點「try it out」。

 

 為 name 和 age 參數填上值,執行。

 

 

下面再舉一例,請求方式為 GET,參數來自 URL 查詢(即帶 ? 的URL,如 /abc?t=3000)。

app.MapGet("/md5", ([FromQuery(Name = "msg")] string data) =>
{
    byte[] buffer = Encoding.UTF8.GetBytes(data);
    using MD5 md5ec = MD5.Create();
    byte[] comres = md5ec.ComputeHash(buffer);
    return $"加密結果:{Convert.ToHexString(comres).ToLower()}";
});

這個 API 的功能:接收一個字元串類型的對象,對其作 MD5 運算,然後返回結果。這個請求是從查詢字元串中得到參數 data 的值的,所以要加上 FromQuery 特性,而且,實際傳值時查詢參數的名稱與API的參數名稱不同,故要用 Name = .... 明確指定,要從 msg 查詢參數中提取值。

綜上,此API的調用方式為 GET /md5?msg=呵呵哈哈呵呵哈。

 

 

咱們玩這一步了,你心中一定有個高大上的疑問:這貨支援依賴注入乎?

很好,老周也有此疑問,要不,咱們搞搞看。

public interface IComputer
{
    int RunIt(int x, int y, int z);
}

internal class ComputerService : IComputer
{
    public int RunIt(int x, int y, int z)
    {
        return x - y - z;
    }
}

我定義了一個服務介面,裡面有個 RunIt 方法;然後俺實現之。邏輯很簡單,x、y、z 三數相減。

接下來改程式碼,在 build 方法調用之前註冊服務,咱們就註冊個單實例模式吧,全局共享一個實例。

var builder = WebApplication.CreateBuilder(args);
……
builder.Services.AddSingleton<IComputer, ComputerService>();

然後,我們完成 API。

app.MapPost("/comp", (int n1, int n2, int n3, IComputer compsv) =>
{
    int r = compsv.RunIt(n1, n2, n3);
    return $"計算結果:{r}";
});

不要猶豫,運行它!

 

 得到結果:

 

 

在這個API中,n1,n2,n3 三個參數是從客戶端 POST 過來的,而最後一個參數是通過依賴注入得到引用對象的。那麼,把參數的位置調換一下,是否也可行呢?

app.MapPost("/comp", (IComputer compsv, int n1, int n2, int n3) =>
{
    int r = compsv.RunIt(n1, n2, n3);
    return $"計算結果:{r}";
});

然後再測,結果表明,是可行滴。

 

 

好了,今天的話題就聊到這兒了,下次咱們就用這個 Mini API 來封裝 MPD。