關於.NET6 Minimal API的使用方式詳解

前言

隨著.Net6的發佈,微軟也改進瞭對之前ASP.NET Core構建方式,使用瞭新的Minimal API模式。之前默認的方式是需要在Startup中註冊IOC和中間件相關,但是在Minimal API模式下你隻需要簡單的寫幾行代碼就可以構建一個ASP.NET Core的Web應用,真可謂非常的簡單,加之配合c#的global using和Program的頂級聲明方式,使得Minimal API變得更為簡潔,不得不說.NET團隊在,NET上近幾年真是下瞭不少功夫,接下來我們就來大致介紹下這種極簡的使用模式。

使用方式

既然說它很簡單瞭,到底是怎麼個簡單法呢。相信下載過Visual Studio 2022的同學們已經用它新建過ASP.NET Core 6的項目瞭,默認的方式就是Minimal API模式,這樣讓整個Web程序的結構看起來更簡單瞭,加上微軟對Lambda的改進使其可以對Lambda參數進行Attribute標記,有的場景甚至可以放棄去定義Controller類瞭。

幾行代碼構建Web程序

使用Minimal API最簡單的方式就是能通過三行代碼就可以構建一個WebApi的程序,代碼如下

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World");
app.Run();

是的你沒有看錯,僅僅這樣運行起來就可以,默認監聽的 http://localhost:5000https://localhost:5001,所以直接在瀏覽器輸入http://localhost:5000地址就可以看到瀏覽器輸出Hello World字樣。

更改監聽地址

如果你想更改它監聽的服務端口可以使用如下的方式進行更改

var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World");
app.Run("http://localhost:6666");

如果想同時監聽多個端口的話,可以使用如下的方式

var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:6666");
app.Urls.Add("http://localhost:8888");
app.MapGet("/", () => "Hello World");
app.Run();

或者是直接通過環境變量的方式設置監聽信息,設置環境變量ASPNETCORE_URLS的值為完整的監聽URL地址,這樣的話就可以直接省略瞭在程序中配置相關信息瞭

ASPNETCORE_URLS=http://localhost:6666

如果設置多個監聽的URL地址的話可以在多個地址之間使用分號;隔開多個值

ASPNETCORE_URLS=http://localhost:6666;https://localhost:8888

如果想監聽本機所有Ip地址則可以使用如下方式

var app = WebApplication.Create(args);
app.Urls.Add("http://*:6666");
app.Urls.Add("http://+:8888");
app.Urls.Add("http://0.0.0.0:9999");
app.MapGet("/", () => "Hello World");
app.Run();

同樣的也可以使用添加環境變量的方式添加監聽地址

ASPNETCORE_URLS=http://*:6666;https://+:8888;http://0.0.0.0:9999

日志操作

日志操作也是比較常用的操作,在Minimal API中微軟幹脆把它提出來,直接簡化瞭操作,如下所示

var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.Logger.LogInformation("讀取到的配置信息:{content}", builder.Configuration.GetSection("consul").Get<ConsulOption>());
app.Run();

基礎環境配置

無論我們在之前的.Net Core開發或者現在的.Net6開發都有基礎環境的配置,它包括 ApplicationNameContentRootPath EnvironmentName相關,不過在Minimal API中,可以通過統一的方式去配置

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging
});

Console.WriteLine($"應用程序名稱: {builder.Environment.ApplicationName}");
Console.WriteLine($"環境變量: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot目錄: {builder.Environment.ContentRootPath}");

var app = builder.Build();

或者是通過環境變量的方式去配置,最終實現的效果都是一樣的

  • ASPNETCORE_ENVIRONMENT
  • ASPNETCORE_CONTENTROOT
  • ASPNETCORE_APPLICATIONNAME

主機相關設置

我們在之前的.Net Core開發模式中,程序的啟動基本都是通過構建主機的方式,比如之前的Web主機或者後來的泛型主機,在Minimal API中同樣可以進行這些操作,比如我們模擬一下之前泛型主機配置Web程序的方式

var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureDefaults(args).ConfigureWebHostDefaults(webBuilder =>
{
    webBuilder.UseStartup<Startup>();
});

var app = builder.Build();

如果隻是配置Web主機的話Minimal API還提供瞭另一種更直接的方式,如下所示

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseStartup<Startup>();
builder.WebHost.UseWebRoot("webroot");

var app = builder.Build();

默認容器替換

很多時候我們在使用IOC的時候會使用其他三方的IOC框架,比如大傢耳熟能詳的Autofac,我們之前也介紹過其本質方式就是使用UseServiceProviderFactory中替換容器的註冊和服務的提供,在Minimal API中可以使用如下的方式去操作

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
//之前在Startup中配置ConfigureContainer可以使用如下方式
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

中間件相關

相信大傢都已經仔細看過瞭WebApplication.CreateBuilder(args).Build()通過這種方式構建出來的是一個WebApplication類的實例,而WebApplication正是實現瞭 IApplicationBuilder接口。所以其本質還是和我們之前使用Startup中的Configure方法的方式是一致的,比如我們配置一個Swagger程序為例

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
//判斷環境變量
if (app.Environment.IsDevelopment())
{
    //異常處理中間件
    app.UseDeveloperExceptionPage();
    app.UseSwagger();
    app.UseSwaggerUI();
}
//啟用靜態文件
app.UseStaticFiles();

app.UseAuthorization();
app.MapControllers();

app.Run();

常用的中間件配置還是和之前是一樣的,因為本質都是IApplicationBuilder的擴展方法,我們這裡簡單列舉一下

中間件名稱 描述 API
Authentication 認證中間件 app.UseAuthentication()
Authorization 授權中間件. app.UseAuthorization()
CORS 跨域中間件. app.UseCors()
Exception Handler 全局異常處理中間件. app.UseExceptionHandler()
Forwarded Headers 代理頭信息轉發中間件. app.UseForwardedHeaders()
HTTPS Redirection Https重定向中間件. app.UseHttpsRedirection()
HTTP Strict Transport Security (HSTS) 特殊響應頭的安全增強中間件. app.UseHsts()
Request Logging HTTP請求和響應日志中間件. app.UseHttpLogging()
Response Caching 輸出緩存中間件. app.UseResponseCaching()
Response Compression 響應壓縮中間件. app.UseResponseCompression()
Session Session中間件 app.UseSession()
Static Files 靜態文件中間件. app.UseStaticFiles(), app.UseFileServer()
WebSockets WebSocket支持中間件. app.UseWebSockets()

請求處理

我們可以使用WebApplication中的Map{HTTPMethod}相關的擴展方法來處理不同方式的Http請求,比如以下示例中處理Get、Post、Put、Delete相關的請求

app.MapGet("/", () => "Hello GET");
app.MapPost("/", () => "Hello POST");
app.MapPut("/", () => "Hello PUT");
app.MapDelete("/", () => "Hello DELETE");

如果想讓一個路由地址可以處理多種Http方法的請求可以使用MapMethods方法,如下所示

app.MapMethods("/multiple", new[] { "GET", "POST","PUT","DELETE" }, (HttpRequest req) => $"Current Http Method Is {req.Method}" );

通過上面的示例我們不僅看到瞭處理不同Http請求的方式,還可以看到Minimal Api可以根據委托的類型自行推斷如何處理請求,比如上面的示例,我們沒有寫Response Write相關的代碼,但是輸出的卻是委托裡的內容,因為我們上面示例中的委托都滿足Func<string>的形式,所以Minimal Api自動處理並輸出返回的信息,其實隻要滿足委托類型的它都可以處理,接下來咱們來簡單一下,首先是本地函數的形式

static string LocalFunction() => "This is local function";
app.MapGet("/local-fun", LocalFunction);

還可以是類的實例方法

HelloHandler helloHandler = new HelloHandler();
app.MapGet("/instance-method", helloHandler.Hello);

class HelloHandler
{
    public string Hello()
    {
        return "Hello World";
    }
}

亦或者是類的靜態方法

app.MapGet("/static-method", HelloHandler.SayHello);

class HelloHandler
{
    public static string SayHello(string name)
    {
        return $"Hello {name}";
    }
}

其實本質都是一樣的,那就是將他們轉換為可執行的委托,無論什麼樣的形式,能滿足委托的條件即可。

路由約束

Minimal Api還支持在對路由規則的約束,這個和我們之前使用UseEndpoints的方式類似,比如我約束路由參數隻能為整型,如果不滿足的話會返回404

app.MapGet("/users/{userId:int}", (int userId) => $"user id is {userId}");
app.MapGet("/user/{name:length(20)}", (string name) => $"user name is {name}");

經常使用的路由約束還有其他幾個,也不是很多大概有如下幾種,簡單的列一下表格

限制 示例 匹配示例 說明
int {id:int} 123456789, -123456789 匹配任何整數
bool {active:bool} true, false 匹配 true 或 false. 忽略大小寫
datetime {dob:datetime} 2016-12-31, 2016-12-31 7:32pm 匹配滿足DateTime類型的值
decimal {price:decimal} 49.99, -1,000.01 匹配滿足 decimal類型的值
double {height:double} 1.234, -1,001.01e8 匹配滿足 double 類型的值
float {height:float} 1.234, -1,001.01e8 匹配滿足 float 類型的值
guid {id:guid} CD2C1638-1638-72D5-1638-DEADBEEF1638 匹配滿足Guid類型的值
long {ticks:long} 123456789, -123456789 匹配滿足 long 類型的值
minlength(value) {username:minlength(4)} KOBE 字符串長度必須是4個字符
maxlength(value) {filename:maxlength(8)} CURRY 字符串長度不能超過8個字符
length(length) {filename:length(12)} somefile.txt 字符串的字符長度必須是12個字符
length(min,max) {filename:length(8,16)} somefile.txt 字符串的字符長度必須介於8和l6之間
min(value) {age:min(18)} 20 整數值必須大於18
max(value) {age:max(120)} 119 整數值必須小於120
range(min,max) {age:range(18,120)} 100 整數值必須介於18和120之間
alpha {name:alpha} Rick 字符串必須由一個或多個a-z的字母字符組成,且不區分大小寫。
regex(expression) {ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} 123-45-6789 字符串必須與指定的正則表達式匹配。
required {name:required} JAMES 請求信息必須包含該參數

模型綁定

在我們之前使用ASP.NET Core Controller方式開發的話,模型綁定是肯定會用到的,它的作用就是簡化我們解析Http請求信息也是MVC框架的核心功能,它可以將請求信息直接映射成c#的簡單類型或者POCO上面。在Minimal Api的Map{HTTPMethod}相關方法中同樣可以進行豐富的模型綁定操作,目前可以支持的綁定源有如下幾種

  • Route(路由參數)
  • QueryString
  • Header
  • Body(比如JSON)
  • Services(即通過IServiceCollection註冊的類型)
  • 自定義綁定

綁定示例

接下來我們首先看一下綁定路由參數

app.MapGet("/sayhello/{name}", (string name) => $"Hello {name}");

還可以使用路由和querystring的混用方式

app.MapGet("/sayhello/{name}", (string name,int? age) => $"my name is {name},age {age}");

這裡需要註意的是,我的age參數加瞭可以為空的標識,如果不加的話則必須要在url的請求參數中傳遞age參數,否則將報錯,這個和我們之前的操作還是有區別的。

具體的類也可以進行模型綁定,比如咱們這裡定義瞭名為Goods的POCO進行演示

app.MapPost("/goods",(Goods goods)=>$"商品{goods.GName}添加成功");

class Goods
{
    public int GId { get; set; }
    public string GName { get; set; }
    public decimal Price { get; set; }
}

需要註意的是HTTP方法GET、HEAD、OPTIONS、DELETE將不會從body進行模型綁定,如果需要在Get請求中獲取Body信息,可以直接從HttpRequest中讀取它。

如果我們需要使用通過IServiceCollection註冊的具體實例,可以以通過模型綁定的方式進行操作(很多人喜歡叫它方法註入,但是嚴格來說卻是是通過定義模型綁定的相關操作實現的),而且還簡化瞭具體操作,我們就不需要在具體的參數上進行FromServicesAttribute標記瞭

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<Person>(provider => new() { Id = 1, Name = "yi念之間", Sex = "Man" });
var app = builder.Build();

app.MapGet("/", (Person person) => $"Hello {person.Name}!");
app.Run();

如果是混合使用的話,也可以不用指定具體的BindSource進行標記瞭,前提是這些值的名稱在不同的綁定來源中是唯一的,這種感覺讓我想到瞭剛開始學習MVC4.0的時候模型綁定的隨意性,比如下面的例子

app.MapGet("/sayhello/{name}", (string name,int? age,Person person) => $"my name is {name},age {age}, sex {person.Sex}");

上面示例的模型綁定參數來源可以

參數 綁定來源
name 路由參數
age querystring
person 依賴註入

不僅僅如此,它還支持更復雜的方式,這使得模型綁定更為靈活,比如以下示例

app.MapPost("/goods",(Goods goods, Person person) =>$"{person.Name}添加商品{goods.GName}成功");

它的模型綁定的值來源可以是

參數 綁定來源
goods body裡的json
person 依賴註入

當然如果你想讓模型綁定的來源更清晰,或者就想指定具體參數的綁定來源那也是可以的,反正就是各種靈活,比如上面的示例改造一下,這樣就可以顯示聲明

app.MapPost("/goods",([FromBody]Goods goods, [FromServices]Person person) =>$"{person.Name}添加商品{goods.GName}成功");

很多時候我們可能通過定義類和方法的方式來聲明Map相關方法的執行委托,這個時候呢依然可以進行靈活的模型綁定,而且可能你也發現瞭,直接通過lambda表達式的方式雖然支持可空類型,但是它不支持缺省參數,也就是咱們說的方法默認參數的形式,比如

app.MapPost("/goods", GoodsHandler.AddGoods);

class GoodsHandler
{
    public static string AddGoods(Goods goods, Person person, int age = 20) => $"{person.Name}添加商品{goods.GName}成功";
}

當然你也可以對AddGoods方法的參數進行顯示的模型綁定處理,真的是十分的靈活

public static string AddGoods([FromBody] Goods goods, [FromServices] Person person, [FromQuery]int age = 20) => $"{person.Name}添加商品{goods.GName}成功";

在使用Map相關方法的時候,由於是在Program入口程序或者其他POCO中直接編寫相關邏輯的,因此需要用到HttpContext、HttpRequest、HttpResponse相關實例的時候沒辦法進行直接操作,這個時候也需要通過模型綁定的方式獲取對應實例

app.MapGet("/getcontext",(HttpContext context,HttpRequest request,HttpResponse response) => response.WriteAsync($"IP:{context.Connection.RemoteIpAddress},Request Method:{request.Method}"));

自定義綁定

Minimal Api采用瞭一種新的方式來自定義模型綁定,這種方式是一種基於約定的方式,無需提前註冊,也無需集成什麼類或者實現什麼接口,隻需要在自定義的類中存在TryParseBindAsync方法即可,這兩個方法的區別是

  • TryParse方法是對路由參數、url參數、header相關的信息進行轉換綁定
  • BindAsync可以對任何請求的信息進行轉換綁定,功能比TryParse要強大

接下來我們分別演示一下這兩種方式的使用方法,首先是TryParse方法

app.MapGet("/address/getarray",(Address address) => address.Addresses);

public class Address
{
    public List<string>? Addresses { get; set; }

    public static bool TryParse(string? addressStr, IFormatProvider? provider, out Address? address)
    {
        var addresses = addressStr?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (addresses != null && addresses.Any())
        {
            address = new Address { Addresses = addresses.ToList() };
            return true;
        }
        address = new Address();
        return false;
    }
}

這樣就可以完成簡單的轉換綁定操作,從寫法上我們可以看到,TryParse方法確實存在一定的限制,不過操作起來比較簡單,這個時候我們模擬請求

http://localhost:5036/address/getarray?address=山東,山西,河南,河北

請求完成會得到如下結果

[“山東”,”山西”,”河南”, “河北”]

然後我們改造一下上面的例子使用BindAsync的方式進行結果轉換,看一下它們操作的不同

app.MapGet("/address/getarray",(Address address) => address.Addresses);

public class Address
{
    public List<string>? Addresses { get; set; }

    public static ValueTask<Address?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        string addressStr = context.Request.Query["address"];
        var addresses = addressStr?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        Address address = new();
        if (addresses != null && addresses.Any())
        {
            address.Addresses = addresses.ToList();
            return ValueTask.FromResult<Address?>(address);
        }
        return ValueTask.FromResult<Address?>(address);
    }
}

同樣請求http://localhost:5036/address/getarray?address=山東,山西,河南,河北 地址會得到和上面相同的結果,到底如何選擇同學們可以按需使用,得到的效果都是一樣的。如果類中同時存在TryParseBindAsync方法,那麼隻會執行BindAsync方法。

輸出結果

相信通過上面的其他示例演示,我們大概看到瞭一些在Minimal Api中的結果輸出,總結起來其實可以分為三種情況

  • IResult 結果輸出,可以包含任何值得輸出,包含異步任務
  • Task<IResult>ValueTask<IResult>string 文本類型輸出,包含異步任務
  • Task<string>ValueTask<string>T 對象類型輸出,比如自定義的實體、匿名對象等,包含異步任務 Task<T>ValueTask<T>

接下來簡單演示幾個例子來簡單看一下具體是如何操作的,首先最簡單的就是輸出文本類型

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

然後輸出一個對象類型,對象類型可以包含對象或集合甚至匿名對象,或者是咱們上面演示過的HttpResponse對象,這裡的對象可以理解為面向對象的那個對象,滿足Response輸出要求即可

app.MapGet("/simple", () => new { Message = "Hello World" });
//或者是
app.MapGet("/array",()=>new string[] { "Hello", "World" });
//亦或者是EF的返回結果
app.Map("/student",(SchoolContext dbContext,int classId)=>dbContext.Student.Where(i=>i.ClassId==classId));

還有一種是微軟幫我們封裝好的一種形式,即返回的是IResult類型的結果,微軟也是很貼心的為我們統一封裝瞭一個靜態的Results類,方便我們使用,簡單演示一下這種操作

//成功結果
app.MapGet("/success",()=> Results.Ok("Success"));
//失敗結果
app.MapGet("/fail", () => Results.BadRequest("fail"));
//404結果
app.MapGet("/404", () => Results.NotFound());
//根據邏輯判斷返回
app.Map("/student", (SchoolContext dbContext, int classId) => {
    var classStudents = dbContext.Student.Where(i => i.ClassId == classId);
    return classStudents.Any() ? Results.Ok(classStudents) : Results.NotFound();
});

上面我們也提到瞭Results類其實是微軟幫我們多封裝瞭一層,它裡面的所有靜態方法都是返回IResult的接口實例,這個接口有許多實現的類,滿足不同的輸出結果,比如Results.File("foo.text")方法其本質就是返回一個FileContentResult類型的實例

public static IResult File(byte[] fileContents,string? contentType = null,
string? fileDownloadName = null,
bool enableRangeProcessing = false,
DateTimeOffset? lastModified = null,
EntityTagHeaderValue? entityTag = null)
=> new FileContentResult(fileContents, contentType)
{
    FileDownloadName = fileDownloadName,
    EnableRangeProcessing = enableRangeProcessing,
    LastModified = lastModified,
    EntityTag = entityTag,
};

亦或者Results.Json(new { Message="Hello World" })本質就是返回一個JsonResult類型的實例

public static IResult Json(object? data, JsonSerializerOptions? options = null, string? contentType = null, int? statusCode = null)
            => new JsonResult
            {
                Value = data,
                JsonSerializerOptions = options,
                ContentType = contentType,
                StatusCode = statusCode,
            };

當然我們也可以自定義IResult的實例,比如我們要輸出一段html代碼。微軟很貼心的為我們提供瞭專門擴展Results的擴展類IResultExtensions基於這個類我們才能完成IResult的擴展

static class ResultsExtensions
{
    //基於IResultExtensions寫擴展方法
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions, nameof(resultExtensions));
        //自定義的HtmlResult是IResult的實現類
        return new HtmlResult(html);
    }
}

class HtmlResult:IResult
{
    //用於接收html字符串
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    /// <summary>
    /// 在該方法寫自己的輸出邏輯即可
    /// </summary>
    /// <returns></returns>
    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

定義完成這些我們就可以直接在Results類中使用我們定義的擴展方法瞭,使用方式如下

app.MapGet("/hello/{name}", (string name) => Results.Extensions.Html(@$"<html>
    <head><title>Index</title></head>
    <body>
        <h1>Hello {name}</h1>
    </body>
</html>"));

這裡需要註意的是,我們自定義的擴展方法一定是基於IResultExtensions擴展的,然後再使用的時候註意是使用的Results.Extensions這個屬性,因為這個屬性是IResultExtensions類型的,然後就是我們自己擴展的Results.Extensions.Html方法。

總結

本文我們主要是介紹瞭ASP.NET Core 6 Minimal API的常用的使用方式,相信大傢對此也有瞭一定的瞭解,在.NET6中也是默認的項目方式,整體來說卻是非常的簡單、簡潔、強大、靈活,不得不說Minimal API卻是在很多場景都非常適用的。當然我也在其它地方看到過關於它的評價,褒貶不一吧,筆者認為,沒有任何一種技術是銀彈,存在即合理。如果你的項目夠規范夠合理,那麼使用Minimal API絕對夠用,如果不想用或者用不瞭也沒關系,能實現你想要結果就好瞭,其實也沒啥好評價的。

到此這篇關於簡單聊下.NET6 Minimal API的使用方式的文章就介紹到這瞭,更多相關.NET6 Minimal API內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: