最小 API 应用中的参数绑定
参数绑定是将请求数据转换为由路由处理程序表示的强类型参数的过程。 绑定源确定绑定参数的位置。 绑定源可以是显式的,也可以是基于 HTTP 方法和参数类型推断的。
绑定优先级
用于从参数确定绑定源的规则:
- 按以下顺序在参数(From* 属性)上定义的显式属性:
- 路由值:
[FromRoute] - 查询字符串:
[FromQuery] - 标头:
[FromHeader] - 正文:
[FromBody] - 窗体:
[FromForm] - 一个服务:
[FromServices] - 参数值:
[AsParameters]
- 路由值:
- 特殊类型
HttpContextHttpRequest(HttpContext.Request)HttpResponse(HttpContext.Response)ClaimsPrincipal(HttpContext.User)CancellationToken(HttpContext.RequestAborted)IFormCollection(HttpContext.Request.Form)IFormFileCollection(HttpContext.Request.Form.Files)IFormFile(HttpContext.Request.Form.Files[paramName])Stream(HttpContext.Request.Body)PipeReader(HttpContext.Request.BodyReader)
- 参数类型具有有效的静态
BindAsync方法。 - 参数类型为字符串或具有有效的静态
TryParse方法。- 如果路由模板中存在参数名称(例如
app.Map("/todo/{id}", (int id) => {});),则将从路由中绑定它。 - 从查询字符串进行绑定。
- 如果路由模板中存在参数名称(例如
- 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
- 参数来自正文。
绑定源
(1)Lambda参数自动绑定
app.MapGet("/search", (string q, int page = 1, int size = 10) =>
{
// q, page, size 自动从查询字符串绑定
return Results.Ok();
});
匹配URL:/serch?q='aaa'&page=2&size=10
(2)显示参数绑定
特性可用于显式声明绑定参数的位置。
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", ([FromRoute] int id,
[FromQuery(Name = "p")] int page,
[FromServices] Service service,
[FromHeader(Name = "Content-Type")] string contentType)
=> {});
class Service { }
record Person(string Name, int Age);
| 参数 | 绑定源 |
|---|---|
id |
名称为 id 的路由值 |
page |
名称为 "p" 的查询字符串 |
service |
由依赖项注入提供 |
contentType |
名称为 "Content-Type" 的标头 |
(3)通过依赖关系注入
将类型配置为服务时,最小 API 的参数绑定通过 依赖注入 绑定参数。 无需将 [FromServices] 属性显式应用于参数。 在以下代码中,这两个操作返回时间:
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();
var app = builder.Build();
app.MapGet("/", ( IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();
(4)可选参数 ?
在路由处理程序中声明的参数被视为必需参数:
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
/products路由未能提供所必须的参数会导致错误:BadHttpRequestException:查询字符串中未提供必需的参数“int pageNumber”。
若要设置为 pageNumber 可选,请将类型定义为可选,或提供默认值:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";
app.MapGet("/products2", ListProducts);
app.Run();
| URI | 结果 |
|---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products2 |
已返回 1 |
前面的可为空默认值适用于所有源:
app.MapPost("/products", (Product? product) => { });
如果未发送请求正文,则前面的代码将使用 null 产品调用方法。
(5)特殊类型
以下类型在绑定时没有显式特性:
-
HttpContext:包含有关当前 HTTP 请求或响应的所有信息的上下文:
app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World")); -
HttpRequest 和 HttpResponse:HTTP 请求和 HTTP 响应:
app.MapGet("/", (HttpRequest request, HttpResponse response) => response.WriteAsync($"Hello World {request.Query["name"]}"));使用 HttpContext 或 HttpRequest 参数直接读取请求正文:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) => { var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName()); await using var writeStream = File.Create(filePath); await request.BodyReader.CopyToAsync(writeStream); }); app.Run(); -
CancellationToken:与当前 HTTP 请求关联的取消标记:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken)); -
ClaimsPrincipal:与请求关联的用户,从 HttpContext.User 进行绑定:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
将请求正文绑定为Sream或PipeReader
例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。
// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>{...});
读取数据时,Stream 是与 HttpRequest.Body 相同的对象。
使用 IFormFile 和 IFormFileCollection 上传文件
在最小化 API 中使用 IFormFile 和 IFormFileCollection 上传文件需要 multipart/form-data 编码。 路由处理程序中的参数名称必须与请求中的窗体字段名称匹配。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/upload", async (IFormFile file) =>
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
});
app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
foreach (var file in myFiles)
{
var tempFile = Path.GetTempFileName();
app.Logger.LogInformation(tempFile);
using var stream = File.OpenWrite(tempFile);
await file.CopyToAsync(stream);
}
});
app.Run();
支持使用 IFormCollection、IFormFile 和 IFormFileCollection 从基于表单的参数进行绑定。
以下代码使用从 IFormFile 类型推断的绑定上传文件:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
//实现表单时,应用必须防止跨网站请求伪造 (XSRF/CSRF) 攻击。 IAntiforgery 服务用于通过生成和验证防伪令牌来防止 XSRF 攻击:
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/upload" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
<input type="file" name="file" placeholder="Upload an image..." accept=".jpg,
.jpeg, .png" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>,
BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
app.Run();
(6)绑定到窗体中的连接和复杂类型
以下支持绑定:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html><body>
<form action="/todo" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}"
type="hidden" value="{token.RequestToken}" />
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="checkbox" name="isCompleted" value="true" />
<input type="submit" />
<input name="isCompleted" type="hidden" value="false" />
</form>
</body></html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>>
([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
try
{
await antiforgery.ValidateRequestAsync(context);
return TypedResults.Ok(todo);
}
catch (AntiforgeryValidationException e)
{
return TypedResults.BadRequest("Invalid antiforgery token");
}
});
app.Run();
class Todo
{
public string Name { get; set; } = string.Empty;
public bool IsCompleted { get; set; } = false;
public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}
提交到上述终结点的表单数据示例如下所示:
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
(7)绑定数组和字符串值
下面的代码展示如何将查询字符串绑定到基元类型、字符串数组和 StringValues 数组:
// Bind query string values to a primitive type array.
// GET /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
$"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");
// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
$"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");
以下代码绑定到标头键 X-Todo-Id,并返回具有匹配 Todo 值的 Id 项:
// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
(8)使用 [AsParameters] 对参数列表进行参数绑定
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
可以用struct、class、record替换上述参数:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
class TodoItemRequest
{
public int Id { get; set; } = default!;
public TodoDb Db { get; set; }=default!;
}
record TodoItemRequest(int Id, TodoDb Db);
与 AsParameters 属性一起使用:
app.MapGet("/ap/todoitems/{id}",
async ([AsParameters] TodoItemRequest request) =>
await request.Db.Todos.FindAsync(request.Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
将 struct 和 AsParameters 一起使用可能比使用 record 类型性能更佳。

浙公网安备 33010602011771号