從 MVC 到使用 ASP.NET Core 6.0 的最小 API
本文轉(zhuǎn)載自微信公眾號「DotNET技術(shù)圈」,作者Ben Foster。轉(zhuǎn)載本文請聯(lián)系DotNET技術(shù)圈公眾號。
2007 年,隨著 ASP.NET MVC 引入了其他語言中變得司空見慣的模型-視圖-控制器模式[1],并為其提供原生支持,.NET Web 應(yīng)用程序開發(fā)有了極速的發(fā)展。
2012 年,也許是由于 ReSTful API 的日益流行,借鑒了 ASP.NET MVC 的許多概念又引入了 ASP.NET Web API,這是對 WCF 的重大改進(jìn),使開發(fā)人員能夠以更少的儀式構(gòu)建 HTTP API,。
后來,在 ASP.NET Core 中,用于構(gòu)建網(wǎng)站和 API 的單一框架,這些框架被統(tǒng)一到了 ASP.NET Core MVC 中。
在 ASP.NET Core MVC 應(yīng)用程序中,控制器負(fù)責(zé)接受輸入、執(zhí)行或編排操作并返回響應(yīng)。它是一個功能齊全的框架,通過過濾器、內(nèi)置模型綁定和驗(yàn)證、約定和基于聲明的行為等提供可擴(kuò)展的管道。對于許多人來說,它是構(gòu)建現(xiàn)代 HTTP 應(yīng)用程序的多合一解決方案。
在某些情況下,您可能只需要 MVC 框架的特定功能或具有使 MVC 不受歡迎的性能限制。隨著更多 HTTP 功能作為 ASP.NET Core 中間件(例如身份驗(yàn)證、授權(quán)、路由等)出現(xiàn),無需 MVC 即可構(gòu)建輕量級 HTTP 應(yīng)用程序變得更加容易,但通常需要一些功能,否則您必須自己構(gòu)建,例如作為模型綁定和 HTTP 響應(yīng)生成。
ASP.NET Core 6.0 旨在通過 Minimal API 彌合這一差距,以更少的儀式提供 ASP.NET MVC 的許多功能。這篇文章提供了有關(guān)如何將傳統(tǒng) MVC 概念轉(zhuǎn)換為這種構(gòu)建輕量級 HTTP API 和服務(wù)的新方法的分步指南。
在這些示例中,我使用的是 .NET 6.0 預(yù)覽 7,為了提供公平和最新的并排比較,我還使用了最新的webapi模板,因?yàn)?MVC 還受益于 C# 10 的一些新特性,使事情變得更加“最小化”。
引導(dǎo)
MVC
dotnet new webapi
新的 ASP.NET 模板取消了Startup類并利用了 C# 10 的頂級語句功能,因此我們有一個Program.cs包含所有引導(dǎo)代碼的文件:
- var builder = WebApplication.CreateBuilder(args);
- // Add services to the container.
- builder.Services.AddControllers();
- var app = builder.Build();
- // Configure the HTTP request pipeline.
- if (builder.Environment.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- app.UseHttpsRedirection();
- app.UseAuthorization();
- app.MapControllers();
- app.Run();
調(diào)用builder.Services.AddControllers()負(fù)責(zé)注冊 MVC 框架依賴項(xiàng)并發(fā)現(xiàn)我們的控制器。然后我們調(diào)用app.MapControllers()注冊我們的控制器路由和 MVC 中間件。
最少的API
- dotnet new web
ASP.NET Empty 模板對規(guī)范的“Hello world”示例使用 Minimal API:
- var builder = WebApplication.CreateBuilder(args);
- var app = builder.Build();
- if (app.Environment.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- app.MapGet("/", () => "Hello World!");
- app.Run();
該MapGet方法是 Minimal API 擴(kuò)展的一部分。除此之外,它與 MVC 并沒有太大區(qū)別(考慮到 HTTPS 重定向和授權(quán)中間件只是從 Empty 模板中省略而不是隱式啟用)。
定義路由和處理程序
MVC
在 MVC 中,我們有兩種[2]定義路由的方法,一種是通過約定,一種是使用屬性。
基于約定的路由更常用于網(wǎng)站而不是 API,并包含在mvc模板中。而不是app.MapControllers我們使用:
- app.MapControllerRoute(
- name: "default",
- pattern: "{controller=Home}/{action=Index}/{id?}");
所述pattern指定路線的不同區(qū)段,并且允許指定的默認(rèn)值。參數(shù)可以利用 ASP.NET 的路由約束語法[3]來限制接受的值。
對于 API,建議使用基于屬性的路由[4]。
通過屬性路由,您可以使用指定 HTTP 動詞和路徑的屬性來裝飾控制器和動作:
- [ApiController]
- [Route("[controller]")]
- public class WeatherForecastController : ControllerBase
- {
- private static readonly string[] Summaries = new[]
- {
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
- };
- [HttpGet]
- public IEnumerable<WeatherForecast> Get()
- {
- return Enumerable.Range(1, 5).Select(index => new WeatherForecast
- {
- Date = DateTime.Now.AddDays(index),
- TemperatureC = Random.Shared.Next(-20, 55),
- Summary = Summaries[Random.Shared.Next(Summaries.Length)]
- })
- .ToArray();
- }
- }
在啟動時,路由將自動注冊。上面的示例來自默認(rèn)webapi模板,演示了路由令牌替換。該[Route("[controller]")]屬性將使用/weatherforecast所有路由的前綴(或資源)(控制器類名減去“Controller”后綴),無參數(shù)[HttpGet]屬性將在資源的根處注冊操作,因此HTTP GET /weatherforecast將命中此操作。
如果我想擴(kuò)展 API 以允許按位置檢索預(yù)測,我可以添加以下操作:
- [HttpGet("locations/{location}")]
- public IEnumerable<WeatherForecast> GetByLocation(string location)
- {
- }
請求時,/weatherforecast/locations/london該值london將綁定到相應(yīng)的操作參數(shù)。
與它們的 Minimal API 對應(yīng)物相比,MVC 控制器看起來非常臃腫。但是,值得注意的是,控制器也可以是 POCO(Plain Old CLR Objects)。為了獲得與上面的“Hello World”最小 API 示例相同的結(jié)果,我們只需要:
- public class RootController
- {
- [HttpGet("/")]
- public string Hello() => "Hello World";
- }
從這里你可以看到尤其是當(dāng)你考慮到你仍然需要一定程度的模塊化時,即使使用最小的 API, MVC 也可以是“最小的”,。
最少的API
要使用 Minimal API 定義路由和處理程序,請使用Map(Get|Post|Put|Delete)方法。有趣的是沒有MapPatch方法,但您可以使用MapMethods.
要使用 Minimal API 實(shí)現(xiàn)相同的天氣預(yù)報(bào)示例:
- var summaries = new[]{ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"};app.MapGet("/weatherforecast", () =>{ return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] }) .ToArray();});app.Run();
與 MVC 示例類似,我們可以將其擴(kuò)展為按位置查詢:
- app.MapGet("/weatherforecast/locations/{location}", (string location) =>{});
請注意,在 MVC 和 Minimal API 示例中,我們受益于返回類型到序列化 HTTP 200 (OK) 響應(yīng)的隱式轉(zhuǎn)換。稍后我們將介紹兩個框架的更明確的 HTTP 對象模型。
模型綁定
模型綁定是從 HTTP 請求中檢索值并將它們轉(zhuǎn)換為 .NET 類型的過程。由于我們在上面介紹了綁定路由值,本節(jié)將主要關(guān)注在請求正文中或通過查詢字符串參數(shù)接收 JSON 數(shù)據(jù)。
MVC
在 MVC 中,您可以將 JSON 從請求正文綁定到 .NET 類型,方法是將其作為參數(shù)傳遞給您的操作方法并使用[FromBody]屬性對其進(jìn)行修飾:
- [HttpPost("/payments")]public IActionResult Post([FromBody]PaymentRequest request){ }
或者,通過使用[ApiController]屬性裝飾您的控制器,將應(yīng)用一個約定來綁定主體中的任何復(fù)雜類型。
在某些情況下,您可能希望從查詢參數(shù)綁定復(fù)雜類型。我喜歡為具有多個過濾選項(xiàng)的搜索端點(diǎn)執(zhí)行此操作。您可以使用以下[FromQuery]屬性實(shí)現(xiàn)此目的:
- [HttpGet("/echo")]public IActionResult Search([FromQuery]SearchRequest request){ }
否則,簡單類型將從路由或查詢字符串值綁定:
- [HttpGet("/portfolios/{id}")]public IActionResult Search(int id, int? page = 1, int? pageSize = 10){ }
/portfolios/10?page=2&pagesize=20將滿足上述操作參數(shù)的請求。
上面的示例還通過將可選參數(shù)標(biāo)記為可為空并可選地提供默認(rèn)值來演示可選參數(shù)的使用。
這對于復(fù)雜類型的工作方式略有不同。即使將類型設(shè)為可空,如果未發(fā)送正文,您將收到 HTTP 415(無效媒體類型)或 400(錯誤請求)響應(yīng),具體取決于是否Content-Type設(shè)置了標(biāo)頭。
以前,這種行為只能通過全局進(jìn)行MvcOptions.AllowEmptyInputInBodyModelBinding全局配置,但從 ASP.NET Core 5 開始,它現(xiàn)在可以按請求進(jìn)行配置:
- [HttpPost("/payments")]public IActionResult Post([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]PaymentRequest? request){ }
最少的API
Minimal API 中的模型綁定非常相似;您使用您希望從請求中綁定的類型配置您的處理程序委托。復(fù)雜類型將從請求正文中自動綁定,而簡單類型將從路由或查詢字符串參數(shù)中綁定。使用 Minimal API 實(shí)現(xiàn)的相同示例如下:
- app.MapPost("/payments", (PaymentRequest paymentRequest) => { });app.MapGet("/portfolios/{id}", (int id, int? page, int? pageSize) => {});
為了指定默認(rèn)值,您需要傳遞一個方法作為委托,因?yàn)?C# 尚不支持內(nèi)聯(lián) lambda 函數(shù)的默認(rèn)值:
- app.MapGet("/search/{id}", Search);app.Run();IResult Search(int id, int? page = 1, int? pageSize = 10){}
該[FromQuery]屬性不支持綁定復(fù)雜類型。有可用于自定義模型綁定的擴(kuò)展點(diǎn),我將在后面的文章中介紹。
要支持可選的請求參數(shù),您可以應(yīng)用與[FromBody]MVC相同的屬性,指定EmptyBodyBehavior:
- app.MapPost("/payments", ([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]PaymentRequest? paymentRequest]) => {});
HTTP 響應(yīng)
MVC 和 Minimal API 都會自動將您的返回類型序列化到響應(yīng)正文并返回 HTTP 200 (OK) 響應(yīng),例如:
- // MVC[HttpPost("/echo")]public EchoRequest Echo(EchoRequest echo) => echo;// Minimal APIapp.MapPost("/echo", (EchoRequest echo) => echo);
您還可以返回void或Task返回一個空的 HTTP 200 (OK) 響應(yīng):
- // MVC[HttpPost("/echo")]public void Echo(EchoRequest echo) => {};// Minimal APIapp.MapPost("/echo", (EchoRequest echo) => {});
除了隱式轉(zhuǎn)換之外,MVC 和 Minimal API 都有一個豐富的 HTTP 響應(yīng)對象模型,涵蓋了最常見的 HTTP 響應(yīng)。
MVC
在 MVC 中,您可以返回IActionResult并使用許多內(nèi)置實(shí)現(xiàn),例如AcceptedResult. 如果您是從ControllerBase那里派生控制器的,那么大多數(shù)響應(yīng)類型都可以使用輔助方法:
- [HttpDelete("/projects/{id}")]public IActionResult Delete(int id){ return Accepted();}
最少的API
使用 Minimal API,我們可以返回IResult. 在Results靜態(tài)類可以很容易地產(chǎn)生了一些內(nèi)置的響應(yīng)類型:
- app.MapDelete("/projects/{id}", (int id) =>{ return Results.Accepted();});
依賴注入
MVC
要將依賴項(xiàng)注入 MVC 控制器,我們通常使用構(gòu)造函數(shù)注入,其中所需的類型(或更常見的是它們的底層接口)作為構(gòu)造函數(shù)參數(shù)提供:
- public class CacheController : ControllerBase{ private readonly ICache _cache; public CacheController(ICache cache) { _cache = cache; } [HttpDelete("/cache/{id}")] public async Task<IActionResult> Delete(string id) { await _cache.Delete(id); return Accepted(); }}
依賴項(xiàng)在啟動時注冊(現(xiàn)在默認(rèn)在 Program.cs 中):
- builder.Services.AddScoped<ICache, MemoryCache>();
使用范圍生命周期注冊的服務(wù)將在 MVC 應(yīng)用程序中按 HTTP 請求創(chuàng)建。
最少的API
使用 Minimal API,我們?nèi)匀豢梢詮囊蕾囎⑷胫惺芤?,但不是使用?gòu)造函數(shù)注入,而是在處理程序委托中將依賴作為參數(shù)傳遞:
- app.MapDelete("/cache/{id}", async (string id, ICache cache) =>{ await cache.Delete(id); return Results.Accepted();});
這種方法更純粹,可以使測試更容易。不利的一面是,一旦您獲得多個依賴項(xiàng),您的處理程序定義就會變得非常嘈雜。
最后,雖然依賴在 內(nèi)本地聲明的依賴項(xiàng)可能很誘人Program.cs,但這不僅會使測試變得困難,而且還會導(dǎo)致范圍問題。我建議盡可能利用 DI 容器,即使是單例依賴。
語境
您的 API 可能需要訪問有關(guān) HTTP 請求的其他信息,例如當(dāng)前用戶的標(biāo)頭或詳細(xì)信息。MVC 和 Minimal API 都構(gòu)建在您熟悉的相同 ASP.NET Core HTTP 抽象之上。
MVC
在MVC中,獲得您的控制器時,從ControllerBase您可以訪問HttpContext,HttpRequest,HttpResponse和當(dāng)前用戶(ClaimsPrincipal從基類屬性):
- [HttpGet]public IEnumerable<WeatherForecast> Get(){ if (Request.Headers.TryGetValue("some header", out var headerValue)) { } bool isSpecialUser = User.Identity.IsAuthenticated && User.HasClaim("special");
如果您的控制器是一個簡單的 POCO 并且不是派生自ControllerBase您,則需要使用構(gòu)造函數(shù)注入來注入IHttpContextAccessor您的控制器或直接訪問請求、響應(yīng)和用戶,請為這些類型執(zhí)行一些 DI 連接。如果 POCO 控制器可以利用類似于下面描述的 Minimal API 的方法注入,那就太好了。
最少的API
使用 Minimal API,您可以通過將以下類型之一[5]作為參數(shù)傳遞給處理程序委托來訪問相同的上下文信息:
- HttpContext
- HttpRequest
- HttpResponse
- ClaimsPrincipal
- CancellationToken (請求中止)
- app.MapGet("/hello", (ClaimsPrincipal user) => { return "Hello " + user.FindFirstValue("sub");});
鏈接生成
在某些情況下,您需要生成指向 API 其他部分的鏈接。在 ASP.NET Core 中,我們可以依靠現(xiàn)有的 HTTP 和路由基礎(chǔ)結(jié)構(gòu)來避免對 URI 組件進(jìn)行硬編碼。要生成到已知路線的鏈接,我們首先需要一種方法來識別它們。
MVC
在 MVC 中,我們可以將一個Name屬性傳遞給我們用來裝飾控制器操作的路由屬性,例如:
- [HttpGet("products/{id}", Name = "get_product")]public IActionResult GetProduct(int id){}
然后我們可以使用IUrlHelper生成指向該路由的鏈接:
- [HttpPost("products", Name = "create_product")]public IActionResult CreateProduct(CreateProduct command){ var product = Create(command); return Created(Url.Link("get_product", new { id = product.Id }));}
請注意路由的路由參數(shù)(get_product在本例中為 ID)是如何作為匿名對象傳遞的。
IUrlHelper可通過Url酒店獲得ControllerBase?;蛘?,您可以將它注入到您的類中,前提是您
在
HTTP 范圍內(nèi)。
最少的API
使用 Minimal API,您可以通過附加元數(shù)據(jù)來命名端點(diǎn):
- app.MapGet("/products/{id}", (int id) =>{ return Results.Ok();}).WithMetadata(new EndpointNameMetadata("get_product"));
上述內(nèi)容的簡寫版本W(wǎng)ithName將在未來版本中提供。
還有一個出色的建議[6]是在傳遞方法組而不是內(nèi)聯(lián) lambda 時隱式生成端點(diǎn)名稱。從上面的問題:
- // These endpoints have their name set automaticallyapp.MapGet("/todos/{id}", GetTodoById);async Task<IResult> GetTodoById(int id, TodoDb db){ return await db.Todos.FindAsync(id) is Todo todo ? Results.Ok(todo) : Results.NotFound();};
更新:David Fowler 確認(rèn)這將在 .NET 6 rc1 中可用
命名端點(diǎn)后,您可以注入LinkGenerator處理程序以生成鏈接:
- app.MapPost("payments", async (HttpContext httpContext, IMediator mediator, LinkGenerator links, PaymentRequest payment) =>{ var result = await mediator.Send(payment); return result.Match( invalidRequest => invalidRequest.ToValidationProblem(), success => Results.Created(links.GetUriByName(httpContext, "get_payment", new { id = success.Id})!, payment) );})
一些內(nèi)置的 Result 助手代表你處理這個樣板。同樣的例子,簡化為Results.CreatedAtRoute:
- app.MapPost("payments", async (HttpContext httpContext, IMediator mediator, PaymentRequest payment) =>{ var result = await mediator.Send(payment); return result.Match( invalidRequest => invalidRequest.ToValidationProblem(), success => Results.CreatedAtRoute("get_payment", new { id = success.Id }, success); );})
驗(yàn)證
MVC
輸入驗(yàn)證是任何 API 的重要組成部分。MVC 在 ASP.NET 之上添加的功能之一是模型狀態(tài)。從文檔[7]:
模型狀態(tài)表示來自兩個子系統(tǒng)的錯誤:模型綁定和模型驗(yàn)證。源自模型綁定的錯誤通常是數(shù)據(jù)轉(zhuǎn)換錯誤。
MVC 還包括對通過屬性[8]進(jìn)行驗(yàn)證的內(nèi)置支持,例如:
- public class PaymentRequest{ [Required] public int? Amount { get; set; } [Required] [StringLength(3)] public string Currency { get; set; }}
提示:一個流行的選擇是為Fluent Validation[9]替換基于默認(rèn)屬性的驗(yàn)證。
綁定到此模型類型時,任何驗(yàn)證錯誤都會自動添加到模型狀態(tài)。在控制器中,我們可以檢查它并采取適當(dāng)?shù)拇胧?/p>
- public IActionResult Post(PaymentRequest paymentRequest){ if (!ModelState.IsValid) { // return validation error } // otherwise process}
事實(shí)上,如果我們用[ApiController]約定來裝飾我們的控制器,我們甚至不需要做上面的事情。這將過濾器應(yīng)用于 MVC 管道,該過濾器將驗(yàn)證任何請求的輸入并在必要時返回問題詳細(xì)信息響應(yīng)。
- { "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "One or more validation errors occurred.", "status": 400, "traceId": "00-293242b60c05924743847956126b31fe-a1b01281b398430d-00", "errors": { "Amount": [ "The Amount field is required." ], "Currency": [ "The Currency field is required." ] }}
這是 MVC 過濾器管道如何從您的應(yīng)用程序中刪除重復(fù)的一個很好的例子。過濾器可以訪問您在 ASP.NET 中間件中沒有的其他上下文。這是允許內(nèi)置驗(yàn)證中間件自動執(zhí)行的原因,因?yàn)樗軌?/p>
在
模型綁定發(fā)生
后
運(yùn)行。
最少的API
就目前而言,Minimal API 沒有任何內(nèi)置的驗(yàn)證支持。但是,您當(dāng)然可以自由地推出自己的產(chǎn)品。
Damian Edwards 創(chuàng)建了MinimalValidation[10],這是一個利用類似于默認(rèn) MVC 驗(yàn)證的驗(yàn)證屬性的小型庫:
- app.MapPost("/widgets", (Widget widget) => !MinimalValidation.TryValidate(widget, out var errors) ? Results.BadRequest(errors) : Results.Created($"/widgets/{widget.Name}", widget));app.Run();class Widget{ [Required, MinLength(3)] public string? Name { get; set; } public override string? ToString() => Name;}
您可以在此處[11]找到更多示例。
我個人更喜歡使用Fluent Validation[12]通常用這個庫替換 MVC 中基于屬性的驗(yàn)證。
下面是使用 Fluent Validation 和最少 API 的示例:
- builder.Services.AddValidatorsFromAssemblyContaining<PaymentRequest>(lifetime: ServiceLifetime.Scoped);var app = builder.Build();app.MapPost("payments", async (IValidator<PaymentRequest> validator, PaymentRequest paymentRequest) =>{ ValidationResult validationResult = validator.Validate(paymentRequest); if (!validationResult.IsValid) { return Results.ValidationProblem(validationResult.ToDictionary()); } // otherwise process});// URL generation?app.Run();public record PaymentRequest(int? Amount, string Currency){ public class Validator : AbstractValidator<PaymentRequest> { public Validator() { RuleFor(x => x.Amount).NotNull().WithMessage("amount_required"); RuleFor(x => x.Currency).Length(3).WithMessage("currency_invalid"); } }}public static class ValidationExtensions{ public static IDictionary<string, string[]> ToDictionary(this ValidationResult validationResult) => validationResult.Errors .GroupBy(x => x.PropertyName) .ToDictionary( g => g.Key, g => g.Select(x => x.ErrorMessage).ToArray() );}
注意:FV 驗(yàn)證器不需要嵌套在它們的目標(biāo)類型中。這只是個人喜好。
在這里,我利用 Fluent Validation 的程序集掃描功能來定位我的驗(yàn)證器?;蛘?,我可以IValidator
這里的一個缺點(diǎn)是您可能最終會在每個處理程序中編寫相同的樣板驗(yàn)證檢查??梢酝ㄟ^一些重構(gòu)來減少它,但是沒有可以訪問綁定模型的預(yù)處理程序鉤子,我們不能像使用 MVC 過濾器那樣輕松地短路請求。我將在稍后的博客文章中介紹一些替代方法。
JSON 序列化
您可能需要自定義默認(rèn)的 JSON 序列化設(shè)置以滿足您的需求或 API 樣式指南。例如,默認(rèn)設(shè)置將字段名稱序列化為駝峰式大小寫(即firstName),但我們的 API 標(biāo)準(zhǔn)要求所有 API 都使用蛇形大小寫(即first_name)。
ASP.NET 6.0 使用 System.Text.Json 處理 JSON,自定義選項(xiàng)在此處[13]有詳細(xì)說明。
MVC
在 MVC 中,您可以通過AddJsonOptions擴(kuò)展自定義 JSON :
- services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy());
注意:開箱即用仍不支持蛇形命名法(Snake casing[14],譯者按:當(dāng)今許多編程語言都建議在某些情況下使用類似蛇的命名法,對于單個字符或單詞(例如A,PYTHON,BOY),當(dāng)將它們用作變量名時,大致所有小寫字母,全部大寫字母和首字母大寫字母。但是,編程語言通常需要使用多個單詞或符號來表示變量名中更豐富的含義。英語習(xí)慣于使用空格分隔單詞,但是這種用法會給編程語言帶來麻煩,因此程序員創(chuàng)建了其他方法,蛇形命名法就是用下劃線分隔兩個字符,使其可讀性更強(qiáng))。您可以在此處[15]找到上述策略的源代碼。
最少的 API
最小的 API 依賴于許多擴(kuò)展方法[16]來序列化到/從 JSON。它們允許JsonSerializerOptions提供,但否則會退回到JsonOptions從HttContext.Request.Services. 您可以在啟動時配置這些選項(xiàng):
- builder.Services.Configure<JsonOptions>(opt =>{ opt.SerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy());});
注意,你需要配置的Microsoft.AspNetCore.Http.Json.JsonOptions不是Mvc命名空間下的類。
我在深入研究源代碼時發(fā)現(xiàn)的一件事是,序列化對象ObjectResult[17]的IResult實(shí)現(xiàn)的基類僅支持序列化 JSON。有人告訴我這是設(shè)計(jì)使然,因?yàn)榇蠖鄶?shù)開發(fā)人員很少需要支持其他媒體類型。如果您需要支持內(nèi)容協(xié)商,您可能需要構(gòu)建自己的IResult.
授權(quán)
我想介紹的最后一個功能是授權(quán)。身份驗(yàn)證和授權(quán)都作為中間件存在,可用于任何風(fēng)格的 ASP.NET Core 應(yīng)用程序。
在
添加 MVC 或 Minimal API 中間件
之前
,您需要確保在應(yīng)用程序中同時注冊授權(quán)服務(wù)和中間件:
- var builder = WebApplication.CreateBuilder(args);// Add services to the container.builder.Services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer();builder.Services.AddAuthorization();builder.Services.AddControllers(); // If MVCvar app = builder.Build();// Configure the HTTP request pipeline.if (builder.Environment.IsDevelopment()){ app.UseDeveloperExceptionPage();}app.UseAuthentication();app.UseAuthorization(); // <-- this needs to come firstapp.MapControllers(); // MVCapp.MapGet("/", () => "Hello World!"); // Minimal APIsapp.Run();
上面的例子是使用 JWT Bearer 認(rèn)證。
MVC 和 Minimal API 之間的主要區(qū)別在于您聲明授權(quán)要求的方式。
默認(rèn)安全
如果您對所有端點(diǎn)都有相同的授權(quán)要求,我建議您將回退策略設(shè)置為要求經(jīng)過身份驗(yàn)證的用戶:
- builder.Services.AddAuthorization(options =>{ options.FallbackPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser();})
如果您有其他要求或需要允許對特定端點(diǎn)進(jìn)行匿名訪問,您可以使用以下說明注釋您的端點(diǎn)。
MVC
在 MVC 應(yīng)用程序中,使用[Authorize]屬性裝飾您的控制器和/或操作以指定您的授權(quán)要求。此屬性允許您指定角色和策略。此示例取自Microsoft Docs[18],將AtLeast21策略應(yīng)用于控制器中定義的所有操作:
- [Authorize(Policy = "AtLeast21")]public class AlcoholPurchaseController : Controller{ public IActionResult Index() => Ok();}
如果您的某些 API 端點(diǎn)需要允許匿名訪問,您可以使用以下[AllowAnonymous]屬性裝飾這些操作:
- [AllowAnonymous][HttpGet("/free-for-all")]public IActionResult FreeForAll(){ return Ok();}
最少的API
為了使用 Minimal API 實(shí)現(xiàn)相同的行為,我們可以將額外的元數(shù)據(jù)附加到端點(diǎn),如下所示:
- app.MapGet("/alcohol", () => Results.Ok()) .RequireAuthorization("AtLeast21");
同樣,要允許匿名訪問:
- app.MapGet("/free-for-all", () => Results.Ok()) .AllowAnonymous();
后來我發(fā)現(xiàn)[Authorize]在使用方法組定義處理程序時可以使用與 MVC相同的屬性:
- [Authorize("AtLeast21")]string Alcohol(){ }
包起來
最小 API 提供了一種使用 ASP.NET Core 構(gòu)建 API 的替代方法。盡管很容易將它們視為“代碼較少的 API”,但主要的好處是您擁有一個輕量級的基礎(chǔ),您可以在此基礎(chǔ)上挑選所需的組件,而不是像 MVC 那樣沉重的東西,后者可能包含許多出色的功能你不使用(例如過濾器)。在許多情況下,這可能會導(dǎo)致服務(wù)占用空間小得多,并隨后獲得性能提升。
值得一提的是,過去曾有社區(qū)努力實(shí)現(xiàn)同樣的目標(biāo)。Nancy[19]在 Web API / OWIN 時代為我們提供了類似的東西,最近Carter[20]為 ASP.NET Core 出現(xiàn),提供與 Minimal API 類似的功能。
作為 ASP.NET Core 開發(fā)人員,您現(xiàn)在在如何構(gòu)建 API 方面有多種選擇,這只能是一件好事。
References
[1] 模型-視圖-控制器模式: https://en.wikipedia.org/wiki/Model–view–controller
[2] 兩種: https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0
[3] 路由約束語法: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0#route-constraint-reference
[4] 基于屬性的路由: https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-5.0#attribute-routing-for-rest-apis
[5] 以下類型之一: https://github.com/dotnet/aspnetcore/blob/867cec475d18892b828ac44a82d74eccfbbb0e49/src/Http/Http.Extensions/src/RequestDelegateFactory.cs#L243
[6] 出色的建議: https://github.com/dotnet/aspnetcore/issues/34540
[7] 文檔: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0
[8] 屬性: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#validation-attributes
[9] Fluent Validation: https://fluentvalidation.net/
[10] MinimalValidation: https://github.com/DamianEdwards/MinimalValidation
[11] 在此處: https://github.com/DamianEdwards/MinimalApiPlayground
[12] Fluent Validation: https://fluentvalidation.net/
[13] 此處: https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-configure-options?pivots=dotnet-5-0
[14] Snake casing: https://en.wikipedia.org/wiki/Snake_case
[15] 此處: https://github.com/benfoster/problem-details-demo/blob/main/src/ProblemDetailsDemo/Startup.cs
[16] 擴(kuò)展方法: https://github.com/dotnet/aspnetcore/tree/main/src/Http/Http.Extensions/src
[17] ObjectResult: https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http.Results/src/ObjectResult.cs
[18] Microsoft Docs: https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-5.0
[19] Nancy: https://nancyfx.org/
[20] Carter: https://github.com/CarterCommunity/Carter