關於.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:5000
和 https://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開發都有基礎環境的配置,它包括 ApplicationName
、ContentRootPath
、 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采用瞭一種新的方式來自定義模型綁定,這種方式是一種基於約定的方式,無需提前註冊,也無需集成什麼類或者實現什麼接口,隻需要在自定義的類中存在TryParse
和BindAsync
方法即可,這兩個方法的區別是
- 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=山東,山西,河南,河北
地址會得到和上面相同的結果,到底如何選擇同學們可以按需使用,得到的效果都是一樣的。如果類中同時存在TryParse
和BindAsync
方法,那麼隻會執行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!
推薦閱讀:
- .Net 6簡介並和之前版本寫法做對比
- ASP.NET Core設置URLs的五種方法
- 修改 asp.net core 5 程序的默認端口號
- Java中lombok的@Builder註解的解析與簡單使用詳解
- 使用VS2022在ASP.NET Core中構建輕量級服務