从 MVC 到使用 ASP.NET Core 6.0 的最小 API
https://benfoster.io/blog/mvc-to-minimal-apis-aspnet-6/
2007 年,随着 ASP.NET MVC 引入了其他语言中变得司空见惯的模型-视图-控制器模式,并为其提供原生支持,.NET Web 应用程序开发有了极速的发展。
2012 年,也许是由于 ReSTful API 的日益流行,借鉴了 ASP.NET MVC 的许多概念又引入了 ASP.NET Web API,这是对 WCF 的重大改进,使开发人员能够以更少的仪式构建 HTTP API,。
后来,在 ASP.NET Core 中,用于构建网站和 API 的单一框架,这些框架被统一到了 ASP.NET Core MVC 中。
在 ASP.NET Core MVC 应用程序中,控制器负责接受输入、执行或编排操作并返回响应。它是一个功能齐全的框架,通过过滤器、内置模型绑定和验证、约定和基于声明的行为等提供可扩展的管道。对于许多人来说,它是构建现代 HTTP 应用程序的多合一解决方案。
在某些情况下,您可能只需要 MVC 框架的特定功能或具有使 MVC 不受欢迎的性能限制。随着更多 HTTP 功能作为 ASP.NET Core 中间件(例如身份验证、授权、路由等)出现,无需 MVC 即可构建轻量级 HTTP 应用程序变得更加容易,但通常需要一些功能,否则您必须自己构建,例如作为模型绑定和 HTTP 响应生成。
ASP.NET Core 6.0 旨在通过 Minimal API 弥合这一差距,以更少的仪式提供 ASP.NET MVC 的许多功能。这篇文章提供了有关如何将传统 MVC 概念转换为这种构建轻量级 HTTP API 和服务的新方法的分步指南。
在这些示例中,我使用的是 .NET 6.0 预览 7,为了提供公平和最新的并排比较,我还使用了最新的webapi
模板,因为 MVC 还受益于 C# 10 的一些新特性,使事情变得更加“最小化”。
引导
MVC
dotnet new webapi
新的 ASP.NET 模板取消了Startup
类并利用了 C# 10 的顶级语句功能,因此我们有一个Program.cs
包含所有引导代码的文件:
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();
调用builder.Services.AddControllers()
负责注册 MVC 框架依赖项并发现我们的控制器。然后我们调用app.MapControllers()
注册我们的控制器路由和 MVC 中间件。
最少的API
dotnet new web
ASP.NET Empty 模板对规范的“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 扩展的一部分。除此之外,它与 MVC 并没有太大区别(考虑到 HTTPS 重定向和授权中间件只是从 Empty 模板中省略而不是隐式启用)。
定义路由和处理程序
MVC
在 MVC 中,我们有两种定义路由的方法,一种是通过约定,一种是使用属性。
基于约定的路由更常用于网站而不是 API,并包含在mvc
模板中。而不是app.MapControllers
我们使用:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
所述pattern
指定路线的不同区段,并且允许指定的默认值。参数可以利用 ASP.NET 的路由约束语法来限制接受的值。
对于 API,建议使用基于属性的路由。
通过属性路由,您可以使用指定 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 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();
}
}
在启动时,路由将自动注册。上面的示例来自默认webapi
模板,演示了路由令牌替换。该[Route("[controller]")]
属性将使用/weatherforecast
所有路由的前缀(或资源)(控制器类名减去“Controller”后缀),无参数[HttpGet]
属性将在资源的根处注册操作,因此HTTP GET /weatherforecast
将命中此操作。
如果我想扩展 API 以允许按位置检索预测,我可以添加以下操作:
[HttpGet("locations/{location}")]
public IEnumerable GetByLocation(string location)
{
}
请求时,/weatherforecast/locations/london
该值london
将绑定到相应的操作参数。
与它们的 Minimal API 对应物相比,MVC 控制器看起来非常臃肿。但是,值得注意的是,控制器也可以是 POCO(Plain Old CLR Objects)。为了获得与上面的“Hello World”最小 API 示例相同的结果,我们只需要:
public class RootController
{
[HttpGet("/")]
public string Hello() => "Hello World";
}
从这里你可以看到尤其是当你考虑到你仍然需要一定程度的模块化时,即使使用最小的 API, MVC 也可以是“最小的”,。
最少的API
要使用 Minimal API 定义路由和处理程序,请使用Map(Get|Post|Put|Delete)
方法。有趣的是没有MapPatch
方法,但您可以使用MapMethods
.
要使用 Minimal API 实现相同的天气预报示例:
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 示例类似,我们可以将其扩展为按位置查询:
app.MapGet("/weatherforecast/locations/{location}", (string location) =>{});
请注意,在 MVC 和 Minimal API 示例中,我们受益于返回类型到序列化 HTTP 200 (OK) 响应的隐式转换。稍后我们将介绍两个框架的更明确的 HTTP 对象模型。
模型绑定
模型绑定是从 HTTP 请求中检索值并将它们转换为 .NET 类型的过程。由于我们在上面介绍了绑定路由值,本节将主要关注在请求正文中或通过查询字符串参数接收 JSON 数据。
MVC
在 MVC 中,您可以将 JSON 从请求正文绑定到 .NET 类型,方法是将其作为参数传递给您的操作方法并使用[FromBody]
属性对其进行修饰:
[HttpPost("/payments")]public IActionResult Post([FromBody]PaymentRequest request){ }
或者,通过使用[ApiController]
属性装饰您的控制器,将应用一个约定来绑定主体中的任何复杂类型。
在某些情况下,您可能希望从查询参数绑定复杂类型。我喜欢为具有多个过滤选项的搜索端点执行此操作。您可以使用以下[FromQuery]
属性实现此目的:
[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
将满足上述操作参数的请求。
上面的示例还通过将可选参数标记为可为空并可选地提供默认值来演示可选参数的使用。
这对于复杂类型的工作方式略有不同。即使将类型设为可空,如果未发送正文,您将收到 HTTP 415(无效媒体类型)或 400(错误请求)响应,具体取决于是否Content-Type
设置了标头。
以前,这种行为只能通过全局进行MvcOptions.AllowEmptyInputInBodyModelBinding
全局配置,但从 ASP.NET Core 5 开始,它现在可以按请求进行配置:
[HttpPost("/payments")]public IActionResult Post([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]PaymentRequest? request){ }
最少的API
Minimal API 中的模型绑定非常相似;您使用您希望从请求中绑定的类型配置您的处理程序委托。复杂类型将从请求正文中自动绑定,而简单类型将从路由或查询字符串参数中绑定。使用 Minimal API 实现的相同示例如下:
app.MapPost("/payments", (PaymentRequest paymentRequest) => { });app.MapGet("/portfolios/{id}", (int id, int? page, int? pageSize) => {});
为了指定默认值,您需要传递一个方法作为委托,因为 C# 尚不支持内联 lambda 函数的默认值:
app.MapGet("/search/{id}", Search);app.Run();IResult Search(int id, int? page = 1, int? pageSize = 10){}
该[FromQuery]
属性不支持绑定复杂类型。有可用于自定义模型绑定的扩展点,我将在后面的文章中介绍。
要支持可选的请求参数,您可以应用与[FromBody]
MVC相同的属性,指定EmptyBodyBehavior
:
app.MapPost("/payments", ([FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)]PaymentRequest? paymentRequest]) => {});
HTTP 响应
MVC 和 Minimal API 都会自动将您的返回类型序列化到响应正文并返回 HTTP 200 (OK) 响应,例如:
// MVC[HttpPost("/echo")]public EchoRequest Echo(EchoRequest echo) => echo;// Minimal APIapp.MapPost("/echo", (EchoRequest echo) => echo);
您还可以返回void
或Task
返回一个空的 HTTP 200 (OK) 响应:
// MVC[HttpPost("/echo")]public void Echo(EchoRequest echo) => {};// Minimal APIapp.MapPost("/echo", (EchoRequest echo) => {});
除了隐式转换之外,MVC 和 Minimal API 都有一个丰富的 HTTP 响应对象模型,涵盖了最常见的 HTTP 响应。
MVC
在 MVC 中,您可以返回IActionResult
并使用许多内置实现,例如AcceptedResult
. 如果您是从ControllerBase
那里派生控制器的,那么大多数响应类型都可以使用辅助方法:
[HttpDelete("/projects/{id}")]public IActionResult Delete(int id){ return Accepted();}
最少的API
使用 Minimal API,我们可以返回IResult
. 在Results
静态类可以很容易地产生了一些内置的响应类型:
app.MapDelete("/projects/{id}", (int id) =>{ return Results.Accepted();});
依赖注入
MVC
要将依赖项注入 MVC 控制器,我们通常使用构造函数注入,其中所需的类型(或更常见的是它们的底层接口)作为构造函数参数提供:
public class CacheController : ControllerBase{ private readonly ICache _cache; public CacheController(ICache cache) { _cache = cache; } [HttpDelete("/cache/{id}")] public async Task Delete(string id) { await _cache.Delete(id); return Accepted(); }}
依赖项在启动时注册(现在默认在 Program.cs 中):
builder.Services.AddScoped();
使用范围生命周期注册的服务将在 MVC 应用程序中按 HTTP 请求创建。
最少的API
使用 Minimal API,我们仍然可以从依赖注入中受益,但不是使用构造函数注入,而是在处理程序委托中将依赖作为参数传递:
app.MapDelete("/cache/{id}", async (string id, ICache cache) =>{ await cache.Delete(id); return Results.Accepted();});
这种方法更纯粹,可以使测试更容易。不利的一面是,一旦您获得多个依赖项,您的处理程序定义就会变得非常嘈杂。
最后,虽然依赖在 内本地声明的依赖项可能很诱人Program.cs
,但这不仅会使测试变得困难,而且还会导致范围问题。我建议尽可能利用 DI 容器,即使是单例依赖。
语境
您的 API 可能需要访问有关 HTTP 请求的其他信息,例如当前用户的标头或详细信息。MVC 和 Minimal API 都构建在您熟悉的相同 ASP.NET Core HTTP 抽象之上。
MVC
在MVC中,获得您的控制器时,从ControllerBase
您可以访问HttpContext
,HttpRequest
,HttpResponse
和当前用户(ClaimsPrincipal
从基类属性):
[HttpGet]public IEnumerable Get(){ if (Request.Headers.TryGetValue("some header", out var headerValue)) { } bool isSpecialUser = User.Identity.IsAuthenticated && User.HasClaim("special");
如果您的控制器是一个简单的 POCO 并且不是派生自ControllerBase
您,则需要使用构造函数注入来注入IHttpContextAccessor
您的控制器或直接访问请求、响应和用户,请为这些类型执行一些 DI 连接。如果 POCO 控制器可以利用类似于下面描述的 Minimal API 的方法注入,那就太好了。
最少的API
使用 Minimal API,您可以通过将以下类型之一作为参数传递给处理程序委托来访问相同的上下文信息:
HttpContext
HttpRequest
HttpResponse
ClaimsPrincipal
CancellationToken
(请求中止)
app.MapGet("/hello", (ClaimsPrincipal user) => { return "Hello " + user.FindFirstValue("sub");});
链接生成
在某些情况下,您需要生成指向 API 其他部分的链接。在 ASP.NET Core 中,我们可以依靠现有的 HTTP 和路由基础结构来避免对 URI 组件进行硬编码。要生成到已知路线的链接,我们首先需要一种方法来识别它们。
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 }));}
请注意路由的路由参数(get_product
在本例中为 ID)是如何作为匿名对象传递的。
IUrlHelper
可通过Url
酒店获得ControllerBase
。或者,您可以将它注入到您的类中,前提是您在HTTP 范围内。
最少的API
使用 Minimal API,您可以通过附加元数据来命名端点:
app.MapGet("/products/{id}", (int id) =>{ return Results.Ok();}).WithMetadata(new EndpointNameMetadata("get_product"));
上述内容的简写版本WithName
将在未来版本中提供。
还有一个出色的建议是在传递方法组而不是内联 lambda 时隐式生成端点名称。从上面的问题:
// These endpoints have their name set automaticallyapp.MapGet("/todos/{id}", GetTodoById);async Task GetTodoById(int id, TodoDb db){ return await db.Todos.FindAsync(id) is Todo todo ? Results.Ok(todo) : Results.NotFound();};
更新:David Fowler 确认这将在 .NET 6 rc1 中可用
命名端点后,您可以注入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) );})
一些内置的 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); );})
验证
MVC
输入验证是任何 API 的重要组成部分。MVC 在 ASP.NET 之上添加的功能之一是模型状态。从文档:
模型状态表示来自两个子系统的错误:模型绑定和模型验证。源自模型绑定的错误通常是数据转换错误。
MVC 还包括对通过属性进行验证的内置支持,例如:
public class PaymentRequest{ [Required] public int? Amount { get; set; } [Required] [StringLength(3)] public string Currency { get; set; }}
提示:一个流行的选择是为Fluent Validation替换基于默认属性的验证。
绑定到此模型类型时,任何验证错误都会自动添加到模型状态。在控制器中,我们可以检查它并采取适当的措施:
public IActionResult Post(PaymentRequest paymentRequest){ if (!ModelState.IsValid) { // return validation error } // otherwise process}
事实上,如果我们用[ApiController]
约定来装饰我们的控制器,我们甚至不需要做上面的事情。这将过滤器应用于 MVC 管道,该过滤器将验证任何请求的输入并在必要时返回问题详细信息响应。
{ "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 过滤器管道如何从您的应用程序中删除重复的一个很好的例子。过滤器可以访问您在 ASP.NET 中间件中没有的其他上下文。这是允许内置验证中间件自动执行的原因,因为它能够在模型绑定发生后运行。
最少的API
就目前而言,Minimal API 没有任何内置的验证支持。但是,您当然可以自由地推出自己的产品。
Damian Edwards 创建了MinimalValidation,这是一个利用类似于默认 MVC 验证的验证属性的小型库:
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;}
您可以在此处找到更多示例。
我个人更喜欢使用Fluent Validation通常用这个库替换 MVC 中基于属性的验证。
下面是使用 Fluent Validation 和最少 API 的示例:
builder.Services.AddValidatorsFromAssemblyContaining(lifetime: ServiceLifetime.Scoped);var app = builder.Build();app.MapPost("payments", async (IValidator 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 { 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 ToDictionary(this ValidationResult validationResult) => validationResult.Errors .GroupBy(x => x.PropertyName) .ToDictionary( g => g.Key, g => g.Select(x => x.ErrorMessage).ToArray() );}
注意:FV 验证器不需要嵌套在它们的目标类型中。这只是个人喜好。
在这里,我利用 Fluent Validation 的程序集扫描功能来定位我的验证器。或者,我可以IValidator
显式注册实现。无论哪种方式,这都意味着我的验证器可以提供给我的处理程序,我可以验证传入的类型。
这里的一个缺点是您可能最终会在每个处理程序中编写相同的样板验证检查。可以通过一些重构来减少它,但是没有可以访问绑定模型的预处理程序钩子,我们不能像使用 MVC 过滤器那样轻松地短路请求。我将在稍后的博客文章中介绍一些替代方法。
JSON 序列化
您可能需要自定义默认的 JSON 序列化设置以满足您的需求或 API 样式指南。例如,默认设置将字段名称序列化为驼峰式大小写(即firstName
),但我们的 API 标准要求所有 API 都使用蛇形大小写(即first_name
)。
ASP.NET 6.0 使用 System.Text.Json 处理 JSON,自定义选项在此处有详细说明。
MVC
在 MVC 中,您可以通过AddJsonOptions
扩展自定义 JSON :
services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy());
注意:开箱即用仍不支持蛇形命名法(Snake casing,译者按:当今许多编程语言都建议在某些情况下使用类似蛇的命名法,对于单个字符或单词(例如A,PYTHON,BOY),当将它们用作变量名时,大致所有小写字母,全部大写字母和首字母大写字母。但是,编程语言通常需要使用多个单词或符号来表示变量名中更丰富的含义。 英语习惯于使用空格分隔单词,但是这种用法会给编程语言带来麻烦,因此程序员创建了其他方法,蛇形命名法就是用下划线分隔两个字符,使其可读性更强)。您可以在此处找到上述策略的源代码。
最少的 API
最小的 API 依赖于许多扩展方法来序列化到/从 JSON。它们允许JsonSerializerOptions
提供,但否则会退回到JsonOptions
从HttContext.Request.Services
. 您可以在启动时配置这些选项:
builder.Services.Configure(opt =>{ opt.SerializerOptions.PropertyNamingPolicy = new SnakeCaseNamingPolicy());});
注意,你需要配置的Microsoft.AspNetCore.Http.Json.JsonOptions
不是Mvc
命名空间下的类。
我在深入研究源代码时发现的一件事是,序列化对象ObjectResult
的IResult
实现的基类仅支持序列化 JSON。有人告诉我这是设计使然,因为大多数开发人员很少需要支持其他媒体类型。如果您需要支持内容协商,您可能需要构建自己的IResult
.
授权
我想介绍的最后一个功能是授权。身份验证和授权都作为中间件存在,可用于任何风格的 ASP.NET Core 应用程序。在添加 MVC 或 Minimal API 中间件之前,您需要确保在应用程序中同时注册授权服务和中间件:
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 认证。
MVC 和 Minimal API 之间的主要区别在于您声明授权要求的方式。
默认安全
如果您对所有端点都有相同的授权要求,我建议您将回退策略设置为要求经过身份验证的用户:
builder.Services.AddAuthorization(options =>{ options.FallbackPolicy = new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser();})
如果您有其他要求或需要允许对特定端点进行匿名访问,您可以使用以下说明注释您的端点。
MVC
在 MVC 应用程序中,使用[Authorize]
属性装饰您的控制器和/或操作以指定您的授权要求。此属性允许您指定角色和策略。此示例取自Microsoft Docs,将AtLeast21
策略应用于控制器中定义的所有操作:
[Authorize(Policy = "AtLeast21")]public class AlcoholPurchaseController : Controller{ public IActionResult Index() => Ok();}
如果您的某些 API 端点需要允许匿名访问,您可以使用以下[AllowAnonymous]
属性装饰这些操作:
[AllowAnonymous][HttpGet("/free-for-all")]public IActionResult FreeForAll(){ return Ok();}
最少的API
为了使用 Minimal API 实现相同的行为,我们可以将额外的元数据附加到端点,如下所示:
app.MapGet("/alcohol", () => Results.Ok()) .RequireAuthorization("AtLeast21");
同样,要允许匿名访问:
app.MapGet("/free-for-all", () => Results.Ok()) .AllowAnonymous();
后来我发现[Authorize]
在使用方法组定义处理程序时可以使用与 MVC相同的属性:
[Authorize("AtLeast21")]string Alcohol(){ }
包起来
最小 API 提供了一种使用 ASP.NET Core 构建 API 的替代方法。尽管很容易将它们视为“代码较少的 API”,但主要的好处是您拥有一个轻量级的基础,您可以在此基础上挑选所需的组件,而不是像 MVC 那样沉重的东西,后者可能包含许多出色的功能你不使用(例如过滤器)。在许多情况下,这可能会导致服务占用空间小得多,并随后获得性能提升。
值得一提的是,过去曾有社区努力实现同样的目标。Nancy在 Web API / OWIN 时代为我们提供了类似的东西,最近Carter为 ASP.NET Core 出现,提供与 Minimal API 类似的功能。
作为 ASP.NET Core 开发人员,您现在在如何构建 API 方面有多种选择,这只能是一件好事。如果您希望本文涵盖任何其他功能,请在Twitter 上联系。