最小 API 快速参考
注意
此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本。
警告
此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本。
此文档:
- 提供有关最小 API 的快速参考。
- 适用于经验丰富的开发人员。 有关说明,请参阅教程:使用 ASP.NET Core 创建最小 API。
最小的 API 包括:
WebApplication
以下代码由 ASP.NET Core 模板生成:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
可以通过命令行上的 dotnet new web
或在 Visual Studio 中选择“空 Web”模板来创建前面的代码。
以下代码创建 WebApplication (app
),而无需显式创建 WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
使用预配置默认值初始化 WebApplication 类的新实例。
根据某些条件,WebApplication
会自动在 Minimal API applications
中添加以下中间件:
- 当
HostingEnvironment
为"Development"
时,将首先添加UseDeveloperExceptionPage
。 - 如果用户代码尚未调用
UseRouting
并且配置了终结点(例如app.MapGet
),则其次添加UseRouting
。 - 如果配置了任何终结点,则会在中间件管道的末尾添加
UseEndpoints
。 - 如果用户代码尚未调用
UseAuthentication
,并且如果可以在服务提供商中检测到IAuthenticationSchemeProvider
,则会在UseRouting
后立即添加UseAuthentication
。 使用AddAuthentication
时,默认情况下会添加IAuthenticationSchemeProvider
,并使用IServiceProviderIsService
检测服务。 - 如果用户代码尚未调用
UseAuthorization
,并且如果可以在服务提供商中检测到IAuthorizationHandlerProvider
,则接下来会添加UseAuthorization
。 使用AddAuthorization
时,默认情况下会添加IAuthorizationHandlerProvider
,并使用IServiceProviderIsService
检测服务。 - 在
UseRouting
和UseEndpoints
之间添加用户配置的中间件和终结点。
以下代码实际上是添加到应用程序的自动中间件生成的代码:
if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
if (isAuthenticationConfigured)
{
app.UseAuthentication();
}
if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints
app.UseEndpoints(e => {});
在某些情况下,应用程序的默认中间件配置不正确,需要修改。 例如,应在 UseAuthentication 和 UseAuthorization 前调用 UseCors。 如果调用 UseCors
,应用需要调用 UseAuthentication
和 UseAuthorization
:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
如果在路由匹配发生前应运行中间件,则应调用 UseRouting,并且应在调用 UseRouting
之前放置中间件。 在这种情况下,不需要 UseEndpoints,因为它会自动添加,如上所述:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
添加终端中间件时:
- 必须在
UseEndpoints
后添加中间件。 - 应用需要调用
UseRouting
和UseEndpoints
,以便终端中间件可以放置在正确的位置。
app.UseRouting();
app.MapGet("/", () => "hello world");
app.UseEndpoints(e => {});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
在没有终结点处理请求时运行的中间件。
使用端口
使用 Visual Studio 或 dotnet new
创建 Web 应用时,将创建 Properties/launchSettings.json
文件,该文件指定应用响应的端口。 在后续的端口设置示例中,从 Visual Studio 运行应用会返回错误对话框 Unable to connect to web server 'AppName'
。 Visual Studio 返回错误,因为它需要 Properties/launchSettings.json
中指定的端口,但应用使用的是 app.Run("http://localhost:3000")
指定的端口。 从命令行运行以下端口更改示例。
以下部分设置应用响应的端口。
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
在前面的代码中,应用响应端口 3000
。
多个端口
在以下代码中,应用响应端口 3000
和 4000
。
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
从命令行设置端口
以下命令使应用响应端口 7777
:
dotnet run --urls="https://localhost:7777"
如果在 appsettings.json
文件中也配置了 Kestrel 终结点,则使用 appsettings.json
文件指定的 URL。 有关详细信息,请参阅Kestrel终结点配置
从环境中读取端口
以下代码从环境中读取端口:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
从环境设置端口的首选方法为使用 ASPNETCORE_URLS
环境变量,如以下部分所示。
通过 ASPNETCORE_URLS 环境变量设置端口
ASPNETCORE_URLS
环境变量可用于设置端口:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
支持多个 URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
侦听所有接口
以下示例演示如何侦听所有接口
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用 ASPNETCORE_URLS 侦听所有接口
前面的示例可以使用 ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
使用 ASPNETCORE_HTTPS_PORTS 侦听所有接口
上述示例可以使用 ASPNETCORE_HTTPS_PORTS
和 ASPNETCORE_HTTP_PORTS
。
ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000
有关详细信息,请参阅为 ASP.NET Core Kestrel Web 服务器配置终结点
指定使用开发证书的 HTTPS
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
有关开发证书详细信息,请参阅在 Windows 和 macOS 上信任 ASP.NET Core HTTPS 开发证书。
指定使用自定义证书的 HTTPS
以下部分显示如何使用 appsettings.json
文件和通过配置指定自定义证书。
使用 appsettings.json
指定自定义证书
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
通过配置指定自定义证书
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用证书 API
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
读取环境
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
有关使用环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境
配置
以下代码从配置系统读取:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
有关详细信息,请参阅 ASP.NET Core 中的配置
Logging
以下代码在应用程序启动时将消息写入日志:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录
访问依赖项注入 (DI) 容器
下面的代码演示如何在应用程序启动过程中从 DI 容器获取服务:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
以下代码演示如何使用 [FromKeyedServices]
属性从 DI 容器访问密钥:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
var app = builder.Build();
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
有关 DI 的详细信息,请参阅 ASP.NET Core 中的依赖项注入。
WebApplicationBuilder
本部分包含使用 WebApplicationBuilder 的示例代码。
更改内容根、应用程序名称和环境
以下代码设置内容根、应用程序名称和环境:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder 使用预配置的默认值初始化 WebApplicationBuilder 类的新实例。
有关详细信息,请参阅 ASP.NET Core 基础知识概述
使用环境变量或命令行更改内容根、应用程序名称和环境
下表显示了用于更改内容根、应用程序名称和环境的环境变量及命令行参数:
feature | 环境变量 | 命令行参数 |
---|---|---|
应用程序名称 | ASPNETCORE_APPLICATIONNAME | --applicationName |
环境名称 | ASPNETCORE_ENVIRONMENT | --environment |
内容根 | ASPNETCORE_CONTENTROOT | --contentRoot |
添加配置提供程序
以下示例添加 INI 配置提供程序:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
有关详细信息,请参阅 ASP.NET Core 中的配置中的文件配置提供程序。
读取配置
默认情况下,WebApplicationBuilder 从多个源读取配置,包括:
appSettings.json
和appSettings.{environment}.json
- 环境变量
- 命令行
有关读取的配置源的完整列表,请参阅 ASP.NET Core 中的配置中的默认配置。
以下代码从配置中读取 HelloKey
,并在 /
终结点显示值。 如果配置值为 null,则“Hello”将分配给 message
:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
读取环境
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine($"Running in development.");
}
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
添加日志记录提供程序
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
添加服务
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
自定义 IHostBuilder
可以使用 IHostBuilder访问 IHostBuilder 上的现有扩展方法:
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
自定义 IWebHostBuilder
可以使用 IWebHostBuilder 属性访问 IWebHostBuilder 上的扩展方法。
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
更改 Web 根
默认情况下,Web 根相对于 wwwroot
文件夹中的内容根。 Web 根是静态文件中间件查找静态文件的位置。 可以使用 WebHostOptions
、命令行或 UseWebRoot 方法更改 Web 根:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
自定义依赖项注入 (DI) 容器
下面的示例使用 Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
添加中间件
可以在 WebApplication
上配置任何现有的 ASP.NET Core 中间件:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
有关详细信息,请参阅 ASP.NET Core 中间件
开发人员异常页
WebApplication.CreateBuilder 使用预配置默认值初始化 WebApplicationBuilder 类的新实例。 开发人员异常页在预配置的默认值中启用。 当在开发环境中运行以下代码时,导航到 /
以呈现一个显示异常的友好页面。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ASP.NET Core 中间件
下表列出了一些经常与最小 API 一起使用的中间件。
中间件 | 描述 | API |
---|---|---|
身份验证 | 提供身份验证支持。 | UseAuthentication |
授权 | 提供身份验证支持。 | UseAuthorization |
CORS | 配置跨域资源共享。 | UseCors |
异常处理程序 | 全局处理中间件管道引发的异常。 | UseExceptionHandler |
转接头 | 将代理标头转发到当前请求。 | UseForwardedHeaders |
HTTPS 重定向 | 将所有 HTTP 请求重定向到 HTTPS。 | UseHttpsRedirection |
HTTP 严格传输安全性 (HSTS) | 添加特殊响应标头的安全增强中间件。 | UseHsts |
请求日志记录 | 提供对记录 HTTP 请求和响应的支持。 | UseHttpLogging |
请求超时 | 支持配置请求超时、全局默认值和每个终结点。 | UseRequestTimeouts |
W3C 请求日志记录 | 提供对以 W3C 格式记录 HTTP 请求和响应的支持。 | UseW3CLogging |
响应缓存 | 提供对缓存响应的支持。 | UseResponseCaching |
响应压缩 | 提供对压缩响应的支持。 | UseResponseCompression |
会话 | 提供对管理用户会话的支持。 | UseSession |
静态文件 | 为提供静态文件和目录浏览提供支持。 | UseStaticFiles、UseFileServer |
WebSockets | 启用 WebSockets 协议。 | UseWebSockets |
以下各部分介绍请求处理:路由、参数绑定和响应。
路由
配置的 WebApplication
支持 Map{Verb}
和 MapMethods,其中 {Verb}
是一种采用大小写混写格式的 HTTP 方法,如 Get
、Post
、Put
或 Delete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
传递给这些方法的 Delegate 参数称为“路由处理程序”。
路由处理程序
路由处理程序是在路由匹配时执行的方法。 路由处理程序可以是 Lambda 表达式、本地函数、实例方法或静态方法。 路由处理程序可以是同步的,也可以是异步的。
Lambda 表达式
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
本地函数
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
实例方法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
静态方法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
在 Program.cs
外部定义的终结点
最小 API 不必位于 Program.cs
。
Program.cs
using MinAPISeparateFile;
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}
另请参阅本文后面的路由组。
命名终结点和链接生成
可以为终结点提供名称,以便为终结点生成 URL。 使用命名终结点可避免在应用中使用硬代码路径:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
前面的代码显示来自 /
终结点的 The link to the hello route is /hello
。
注意:终结点名称区分大小写。
终结点名称:
- 必须全局唯一。
- 在启用 OpenAPI 支持时用作 OpenAPI 操作 ID。 有关详细信息,请参阅 OpenAPI。
路由参数
路由参数可以作为路由模式定义的一部分进行捕获:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
前面的代码从 URI /users/3/books/7
返回 The user id is 3 and book id is 7
。
路由处理程序可以声明要捕获的参数。 当向带有声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,userId
和 bookId
均为 int
。
在前面的代码中,如果某个路由值无法转换为 int
,则会引发异常。 GET 请求 /users/hello/books/3
引发了以下异常:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
通配符和“全部捕获”路由
以下“全部捕获”路由从“/posts/hello”终结点返回 Routing to hello
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
路由约束
路由约束限制路由的匹配行为。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
下表显示了前面的路由模板及其行为:
路由模板 | 示例匹配 URI |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
有关详细信息,请参阅 ASP.NET Core 中的路由中的路由约束参考。
路由组
MapGroup 扩展方法有助于组织具有共同前缀的终结点组。 它减少了重复代码,并允许通过对添加终结点元数据的 RequireAuthorization 和 WithMetadata 等方法的单一调用来自定义整个终结点组。
例如,以下代码创建两组相似的终结点:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
在此方案中,可以适用 201 Created
结果中 Location
标头的相对地址:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
第一组终结点将仅匹配前缀为 /public/todos
并且无需任何身份验证即可访问的请求。 第二组终结点将仅匹配前缀为 /private/todos
并且需要身份验证的请求。
QueryPrivateTodos
终结点筛选器工厂是一种本地函数,用于修改路由处理程序的 TodoDb
参数,以允许访问和存储专用的 todo 数据。
路由组还支持嵌套组和具有路由参数和约束的复杂前缀模式。 在以下示例中,映射到 user
组的路由处理程序可以捕获外部组前缀中定义的 {org}
和 {group}
路由参数。
前缀也可以为空。 这对于在不更改路由模式的情况下向一组终结点添加终结点元数据或筛选器非常有用。
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
向组添加筛选器或元数据的行为与在添加可能已添加到内部组或特定终结点的任何额外筛选器或元数据之前将它们单独添加到每个终结点的行为相同。
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
在上面的示例中,外部筛选器将在内部筛选器之前记录传入请求,即使它是第二个添加的。 由于筛选器应用于不同的组,因此它们相对于彼此的添加顺序并不重要。 如果应用于相同的组或特定终结点,则筛选器的添加顺序非常重要。
对 /outer/inner/
的请求将记录以下内容:
/outer group filter
/inner group filter
MapGet filter
参数绑定
参数绑定是将请求数据转换为由路由处理程序表示的强类型参数的过程。 绑定源确定绑定参数的位置。 绑定源可以是显式的,也可以是基于 HTTP 方法和参数类型推断的。
支持的绑定源:
- 路由值
- 查询字符串
- 头文件
- 正文(作为 JSON)
- 窗体值
- 依赖项注入提供的服务
- 自定义
下面的 GET
路由处理程序使用其中一些参数绑定源:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
下表显示了前面的示例中使用的参数与关联的绑定源之间的关系。
参数 | 绑定源 |
---|---|
id |
路由值 |
page |
查询字符串 |
customHeader |
标头的值开始缓存响应 |
service |
由依赖项注入提供 |
HTTP 方法 GET
、HEAD
、OPTIONS
和 DELETE
不会从正文隐式绑定。 如需从这些 HTTP 方法的主体(如 JSON)进行绑定,可以 使用 [FromBody]
显式绑定或从 HttpRequest 读取。
以下示例 POST 路由处理程序将主体(如 JSON)的绑定源用于 person
参数:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
上述示例中的参数都自动从请求数据绑定。 为了演示参数绑定的便利性,以下路由处理程序演示如何直接从请求读取请求数据:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
显式参数绑定
特性可用于显式声明绑定参数的位置。
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" 的标头 |
从窗体值显式绑定
[FromForm]
属性绑定窗体值:
app.MapPost("/todos", async ([FromForm] string name,
[FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
var todo = new Todo
{
Name = name,
Visibility = visibility
};
if (attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await attachment.CopyToAsync(stream);
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
另一种方法是将 [AsParameters]
属性与带有使用 [FromForm]
进行注释的属性的自定义类型一起使用。 例如,以下代码从窗体值绑定到 NewTodoRequest
记录结构的属性:
app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
var todo = new Todo
{
Name = request.Name,
Visibility = request.Visibility
};
if (request.Attachment is not null)
{
var attachmentName = Path.GetRandomFileName();
using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
await request.Attachment.CopyToAsync(stream);
todo.Attachment = attachmentName;
}
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Ok();
});
// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
[FromForm] Visibility Visibility, IFormFile? Attachment);
有关详细信息,请参阅本文后面的 AsParameters 部分。
AspNetCore.Docs.Samples 存储库中的完整示例代码。
IFormFile 和 IFormFileCollection 的安全绑定
可以通过 IFormFile、IFormFileCollection、[FromForm]
来支持复杂表单绑定:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
return Results.Content(html, "text/html");
});
app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
([FromForm] FileUploadForm fileUploadForm, HttpContext context,
IAntiforgery antiforgery) =>
{
await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
fileUploadForm.Name!, app.Environment.ContentRootPath);
return TypedResults.Ok($"Your file with the description:" +
$" {fileUploadForm.Description} has been uploaded successfully");
});
app.Run();
通过 [FromForm]
绑定到请求的参数包括防伪令牌。 处理请求时会验证防伪令牌。 有关详细信息,请参阅用最小 API 实现防伪。
有关详细信息,请参阅最小 API 中的表单绑定。
AspNetCore.Docs.Samples 存储库中的完整示例代码。
通过依赖关系注入进行参数绑定
当类型配置为服务时,最小 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();
可选参数
在路由处理程序中声明的参数被视为必需参数:
- 如果请求与路由匹配,则只有在请求中提供了所有必需的参数时,路由处理程序才会运行。
- 未能提供所有必需的参数会导致错误。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
BadHttpRequestException :查询字符串中未提供必需的参数“int pageNumber”。 |
/products/1 |
HTTP 404 错误,无匹配的路由 |
若要设置为 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 | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products2 |
已返回 1 |
前面的可为空默认值适用于所有源:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
如果未发送请求正文,则前面的代码将使用 null 产品调用方法。
注意:如果提供的数据无效并且参数可为空,则路由处理程序不运行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products?pageNumber=two |
BadHttpRequestException :无法从“two”绑定参数 "Nullable<int> pageNumber" 。 |
/products/two |
HTTP 404 错误,无匹配的路由 |
有关详细信息,请参阅绑定失败部分。
特殊类型
以下类型在绑定时没有显式特性:
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"]}"));
CancellationToken:与当前 HTTP 请求关联的取消标记:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal:与请求关联的用户,从 HttpContext.User 进行绑定:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
将请求正文绑定为 Stream
或 PipeReader
请求正文可以绑定为 Stream
或 PipeReader
,以有效支持用户必须处理数据的情况,以及:
- 将数据存储在 Blob 存储中,或将数据排入队列提供程序的队列。
- 使用工作进程或云功能处理存储的数据。
例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。
以下代码可实现后台队列:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
以下代码将请求正文绑定到 Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
以下代码显示完整的 Program.cs
文件:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// 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) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- 读取数据时,
Stream
是与HttpRequest.Body
相同的对象。 - 默认情况下,不缓冲请求正文。 读取正文后,不支持后退。 无法多次读取流。
- 不能在最小操作处理程序之外使用
Stream
和PipeReader
,因为基础缓冲区将被释放或重用。
使用 IFormFile 和 IFormFileCollection 上传文件
以下代码使用 IFormFile 和 IFormFileCollection 上传文件:
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();
使用授权标头、客户端证书或 cookie 标头支持经过身份验证的文件上传请求。
使用 IFormCollection、IFormFile 和 IFormFileCollection 绑定到表单
支持使用 IFormCollection、IFormFile 和 IFormFileCollection 从基于表单的参数进行绑定。 OpenAPI 元数据针对表单参数进行推断,以支持与 Swagger UI 集成。
以下代码使用从 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);
}
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();
警告:实现表单时,应用必须防止跨网站请求伪造 (XSRF/CSRF) 攻击。 在上述代码中,IAntiforgery 服务用于通过生成和验证防伪令牌来防止 XSRF 攻击:
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);
}
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();
有关 XSRF 攻击的详细信息,请参阅使用最少 API 进行防伪造
有关详细信息,请参阅 Form binding in minimal APIs(最小 API 中的表单绑定);
绑定到窗体中的连接和复杂类型
以下支持绑定:
下面的代码演示:
- 将多部分表单输入绑定到复杂对象的最小终结点。
- 如何使用防伪服务来支持防伪令牌的生成和验证。
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));
}
在上述代码中:
- 必须使用
[FromForm]
属性对目标参数进行批注,以与应从 JSON 正文中读取的参数进行区分。 - 使用请求委托生成器编译的最小 API 不支持从复杂或集合类型进行绑定。
- 该标记显示了一个额外的隐藏输入,其名称为
isCompleted
,值为false
。 如果在提交表单时选中了isCompleted
复选框,则true
和false
两个值都会作为值提交。 如果未选中该复选框,则仅提交隐藏输入值false
。 ASP.NET Core 模型绑定进程在绑定到bool
值时仅读取第一个值,这导致选中的复选框为true
,未选中的复选框为false
。
提交到上述终结点的表单数据示例如下所示:
__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false
绑定标头和查询字符串中的数组和字符串值
下面的代码展示如何将查询字符串绑定到基元类型、字符串数组和 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]}");
在类型实现 TryParse
时,支持将查询字符串或标头值绑定到复杂类型的数组。 以下代码绑定到字符串数组,并返回具有指定标记的所有项:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
以下代码显示了模型和所需的 TryParse
实现:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
以下代码绑定到 int
数组:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
若要测试上述代码,请添加以下终结点以使用 Todo
项填充数据库:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
使用 HttpRepl
之类的工具将以下数据传递给上一终结点:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
以下代码绑定到标头键 X-Todo-Id
,并返回具有匹配 Id
值的 Todo
项:
// 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();
});
注意
从查询字符串绑定 string[]
时,缺少任何匹配的查询字符串值都会导致空数组而不是 null 值。
使用 [AsParameters] 对参数列表进行参数绑定
AsParametersAttribute 启用对类型的简单参数绑定而不是复杂或递归模型绑定。
考虑下列代码:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
请考虑以下 GET
终结点:
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
可用于替换上述突出显示的参数:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
重构的 GET
终结点将上述 struct
与 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());
以下代码显示应用中的其他终结点:
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
以下类用于重构参数列表:
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
以下代码显示了使用 AsParameters
和上述 struct
及类的重构终结点:
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
以下 record
类型可用于替换上述参数:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
将 struct
和 AsParameters
一起使用可能比使用 record
类型性能更佳。
AspNetCore.Docs.Samples 存储库中的完整示例代码。
自定义绑定
自定义参数绑定有两种方法:
- 对于路由、查询和标头绑定源,通过添加类型的静态
TryParse
方法来绑定自定义类型。 - 通过对类型实现
BindAsync
方法来控制绑定过程。
TryParse
TryParse
具有两个 API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
下面的代码显示带有 URI /map?Point=12.3,10.1
的 Point: 12.3, 10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
具有以下 API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
下面的代码显示带有 URI /products?SortBy=xyz&SortDir=Desc&Page=99
的 SortBy:xyz, SortDirection:Desc, CurrentPage:99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
绑定失败
绑定失败时,框架会记录调试消息,并根据失败模式将各种状态代码返回到客户端。
故障模式 | 可为空参数类型 | 绑定源 | 状态代码 |
---|---|---|---|
{ParameterType}.TryParse 返回 false |
是 | route/query/header | 400 |
{ParameterType}.BindAsync 返回 null |
是 | 自定义 | 400 |
{ParameterType}.BindAsync 引发 |
不重要 | custom | 500 |
未能反序列化 JSON 正文 | 不重要 | body | 400 |
错误的内容类型(不是 application/json ) |
不重要 | body | 415 |
绑定优先级
用于从参数确定绑定源的规则:
- 按以下顺序在参数(From* 属性)上定义的显式属性:
- 路由值:
[FromRoute]
- 查询字符串:
[FromQuery]
- 标头:
[FromHeader]
- 正文:
[FromBody]
- 窗体:
[FromForm]
- 一个服务:
[FromServices]
- 参数值:
[AsParameters]
- 路由值:
- 特殊类型
HttpContext
HttpRequest
(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) => {});
),则将从路由中绑定它。 - 从查询字符串进行绑定。
- 如果路由模板中存在参数名称(例如
- 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
- 参数来自正文。
为正文绑定配置 JSON 反序列化选项
正文绑定源使用 System.Text.Json 进行反序列化。 不能更改此默认值,但可以配置 JSON 序列化和反序列化选项。
全局配置 JSON 反序列化选项
全局应用于应用的选项可以通过调用 ConfigureHttpJsonOptions 进行配置。 以下示例包含公共字段,并设置 JSON 输出的格式。
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
由于示例代码同时配置序列化和反序列化,因此它可以读取 NameField
并在输出 JSON 中包含 NameField
。
为终结点配置 JSON 反序列化选项
ReadFromJsonAsync 具有接受 JsonSerializerOptions 对象的重载。 以下示例包含公共字段,并设置 JSON 输出的格式。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
由于上述代码仅将自定义选项应用于反序列化,因此输出 JSON 不包括 NameField
。
读取请求正文
使用 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();
前面的代码:
- 使用 HttpRequest.BodyReader 访问请求正文。
- 将请求正文复制到本地文件。
响应
路由处理程序支持以下类型的返回值:
- 基于
IResult
- 这包括Task<IResult>
和ValueTask<IResult>
ValueTask<string>
- 这包括string
和Task<string>
T
(任何其他类型)- 这包括Task<T>
和ValueTask<T>
返回值 | 行为 | Content-Type |
---|---|---|
IResult |
框架调用 IResult.ExecuteAsync | 由 IResult 实现决定 |
string |
框架将字符串直接写入响应 | text/plain |
T (任何其他类型) |
框架将对响应进行 JSON 序列化 | application/json |
有关路由处理程序返回值的更深入指南,请参阅在最小 API 应用程序中创建响应
示例返回值
字符串返回值
app.MapGet("/hello", () => "Hello World");
JSON 返回值
app.MapGet("/hello", () => new { Message = "Hello World" });
返回 TypedResults
以下代码将返回 TypedResults:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
优先返回 TypedResults
,而不是 Results。 有关详细信息,请参阅 TypedResults 与 Results。
IResult 返回值
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
以下示例使用内置结果类型自定义响应:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
自定义状态代码
app.MapGet("/405", () => Results.StatusCode(405));
文本
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
有关更多示例,请参阅在最小 API 应用程序中创建响应。
重定向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
文件
app.MapGet("/download", () => Results.File("myfile.text"));
内置结果
Results 和 TypedResults 静态类中存在常见的结果帮助程序。 优先返回 TypedResults
,而不是 Results
。 有关详细信息,请参阅 TypedResults 与 Results。
自定义结果
应用程序可以通过实现自定义 IResult 类型来控制响应。 以下代码是 HTML 结果类型的示例:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
建议将扩展方法添加到 Microsoft.AspNetCore.Http.IResultExtensions,使这些自定义结果更容易发现。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
类型化结果
IResult 接口可以表示从最小 API 返回的值,这些 API 不利用对 JSON 将返回的对象序列化为 HTTP 响应的隐式支持。 静态 Results 类用于创建各种 IResult
对象,这些对象表示不同类型的响应。 例如,设置响应状态代码或重定向到另一个 URL。
实现 IResult
的类型是公共类型,允许在测试时使用类型断言。 例如:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
可以查看静态 TypedResults 类上相应方法的返回类型,以找到要转换为的正确公共 IResult
类型。
有关更多示例,请参阅在最小 API 应用程序中创建响应。
筛选器
有关详细信息,请参阅最小 API 筛选器应用。
授权
可以使用授权策略保护路由。 可以通过 [Authorize]
属性或通过使用 RequireAuthorization 方法声明这些对象:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
前面的代码可以用 RequireAuthorization 编写:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
以下示例使用基于策略的授权:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
允许未经身份验证的用户访问终结点
[AllowAnonymous]
允许未经身份验证的用户访问终结点:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
路由可以使用 CORS 策略启用 CORS。 可以通过 [EnableCors]
属性或通过使用 RequireCors 方法声明 CORS。 以下示例启用 CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS)
ValidateScopes 和 ValidateOnBuild
ValidateScopes 和 ValidateOnBuild 在部署环境中默认启用,但在其他环境中禁用。
当 ValidateOnBuild
是 true
时,DI 容器会在生成时验证服务配置。 如果服务配置无效,则生成在应用启动时失败,而不是在运行时请求服务时失败。
当 ValidateScopes
是 true
时,DI 容器会验证是否未从根范围解析范围服务。 从根范围解析范围服务可能会导致内存泄漏,因为服务在内存中保留的时间比请求的范围要长。
出于性能原因,ValidateScopes
和 ValidateOnBuild
在非开发模式下默认为 false。
以下代码显示,ValidateScopes
在开发模式下默认启用,但在发布模式下禁用:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<MyScopedService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
}
else
{
Console.WriteLine("Release environment");
}
app.MapGet("/", context =>
{
// Intentionally getting service provider from app, not from the request
// This causes an exception from attempting to resolve a scoped service
// outside of a scope.
// Throws System.InvalidOperationException:
// 'Cannot resolve scoped service 'MyScopedService' from root provider.'
var service = app.Services.GetRequiredService<MyScopedService>();
return context.Response.WriteAsync("Service resolved");
});
app.Run();
public class MyScopedService { }
以下代码显示,ValidateOnBuild
在开发模式下默认启用,但在发布模式下禁用:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();
// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
}
else
{
Console.WriteLine("Release environment");
}
app.MapGet("/", context =>
{
var service = context.RequestServices.GetRequiredService<MyScopedService>();
return context.Response.WriteAsync("Service resolved correctly!");
});
app.Run();
public class MyScopedService { }
public class AnotherService
{
public AnotherService(BrokenService brokenService) { }
}
public class BrokenService { }
以下代码会在 Development
中禁用 ValidateScopes
和 ValidateOnBuild
:
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
Console.WriteLine("Development environment");
// Doesn't detect the validation problems because ValidateScopes is false.
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = false;
options.ValidateOnBuild = false;
});
}
另请参阅
此文档:
- 提供有关最小 API 的快速参考。
- 适用于经验丰富的开发人员。 有关说明,请参阅教程:使用 ASP.NET Core 创建最小 API
最小的 API 包括:
WebApplication
以下代码由 ASP.NET Core 模板生成:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
可以通过命令行上的 dotnet new web
或在 Visual Studio 中选择“空 Web”模板来创建前面的代码。
以下代码创建 WebApplication (app
),而无需显式创建 WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
使用预配置默认值初始化 WebApplication 类的新实例。
根据某些条件,WebApplication
会自动在 Minimal API applications
中添加以下中间件:
- 当
HostingEnvironment
为"Development"
时,将首先添加UseDeveloperExceptionPage
。 - 如果用户代码尚未调用
UseRouting
并且配置了终结点(例如app.MapGet
),则其次添加UseRouting
。 - 如果配置了任何终结点,则会在中间件管道的末尾添加
UseEndpoints
。 - 如果用户代码尚未调用
UseAuthentication
,并且如果可以在服务提供商中检测到IAuthenticationSchemeProvider
,则会在UseRouting
后立即添加UseAuthentication
。 使用AddAuthentication
时,默认情况下会添加IAuthenticationSchemeProvider
,并使用IServiceProviderIsService
检测服务。 - 如果用户代码尚未调用
UseAuthorization
,并且如果可以在服务提供商中检测到IAuthorizationHandlerProvider
,则接下来会添加UseAuthorization
。 使用AddAuthorization
时,默认情况下会添加IAuthorizationHandlerProvider
,并使用IServiceProviderIsService
检测服务。 - 在
UseRouting
和UseEndpoints
之间添加用户配置的中间件和终结点。
以下代码实际上是添加到应用程序的自动中间件生成的代码:
if (isDevelopment)
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
if (isAuthenticationConfigured)
{
app.UseAuthentication();
}
if (isAuthorizationConfigured)
{
app.UseAuthorization();
}
// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints
app.UseEndpoints(e => {});
在某些情况下,应用程序的默认中间件配置不正确,需要修改。 例如,应在 UseAuthentication 和 UseAuthorization 前调用 UseCors。 如果调用 UseCors
,应用需要调用 UseAuthentication
和 UseAuthorization
:
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
如果在路由匹配发生前应运行中间件,则应调用 UseRouting,并且应在调用 UseRouting
之前放置中间件。 在这种情况下,不需要 UseEndpoints,因为它会自动添加,如上所述:
app.Use((context, next) =>
{
return next(context);
});
app.UseRouting();
// other middleware and endpoints
添加终端中间件时:
- 必须在
UseEndpoints
后添加中间件。 - 应用需要调用
UseRouting
和UseEndpoints
,以便终端中间件可以放置在正确的位置。
app.UseRouting();
app.MapGet("/", () => "hello world");
app.UseEndpoints(e => {});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.CompletedTask;
});
在没有终结点处理请求时运行的中间件。
使用端口
使用 Visual Studio 或 dotnet new
创建 Web 应用时,将创建 Properties/launchSettings.json
文件,该文件指定应用响应的端口。 在后续的端口设置示例中,从 Visual Studio 运行应用会返回错误对话框 Unable to connect to web server 'AppName'
。 Visual Studio 返回错误,因为它需要 Properties/launchSettings.json
中指定的端口,但应用使用的是 app.Run("http://localhost:3000")
指定的端口。 从命令行运行以下端口更改示例。
以下部分设置应用响应的端口。
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
在前面的代码中,应用响应端口 3000
。
多个端口
在以下代码中,应用响应端口 3000
和 4000
。
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
从命令行设置端口
以下命令使应用响应端口 7777
:
dotnet run --urls="https://localhost:7777"
如果在 appsettings.json
文件中也配置了 Kestrel 终结点,则使用 appsettings.json
文件指定的 URL。 有关详细信息,请参阅Kestrel终结点配置
从环境中读取端口
以下代码从环境中读取端口:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
从环境设置端口的首选方法为使用 ASPNETCORE_URLS
环境变量,如以下部分所示。
通过 ASPNETCORE_URLS 环境变量设置端口
ASPNETCORE_URLS
环境变量可用于设置端口:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
支持多个 URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
有关使用环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境
侦听所有接口
以下示例演示如何侦听所有接口
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用 ASPNETCORE_URLS 侦听所有接口
前面的示例可以使用 ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
指定使用开发证书的 HTTPS
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
有关开发证书详细信息,请参阅在 Windows 和 macOS 上信任 ASP.NET Core HTTPS 开发证书。
指定使用自定义证书的 HTTPS
以下部分显示如何使用 appsettings.json
文件和通过配置指定自定义证书。
使用 appsettings.json
指定自定义证书
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
通过配置指定自定义证书
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用证书 API
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
配置
以下代码从配置系统读取:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Config failed!";
app.MapGet("/", () => message);
app.Run();
有关详细信息,请参阅 ASP.NET Core 中的配置
Logging
以下代码在应用程序启动时将消息写入日志:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录
访问依赖项注入 (DI) 容器
下面的代码演示如何在应用程序启动过程中从 DI 容器获取服务:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
有关详细信息,请参阅 ASP.NET Core 中的依赖项注入。
WebApplicationBuilder
本部分包含使用 WebApplicationBuilder 的示例代码。
更改内容根、应用程序名称和环境
以下代码设置内容根、应用程序名称和环境:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder 使用预配置的默认值初始化 WebApplicationBuilder 类的新实例。
有关详细信息,请参阅 ASP.NET Core 基础知识概述
按环境变量或命令行更改内容根、应用程序名称和环境
下表显示了用于更改内容根、应用程序名称和环境的环境变量及命令行参数:
feature | 环境变量 | 命令行参数 |
---|---|---|
应用程序名称 | ASPNETCORE_APPLICATIONNAME | --applicationName |
环境名称 | ASPNETCORE_ENVIRONMENT | --environment |
内容根 | ASPNETCORE_CONTENTROOT | --contentRoot |
添加配置提供程序
以下示例添加 INI 配置提供程序:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
有关详细信息,请参阅 ASP.NET Core 中的配置中的文件配置提供程序。
读取配置
默认情况下,WebApplicationBuilder 从多个源读取配置,包括:
appSettings.json
和appSettings.{environment}.json
- 环境变量
- 命令行
以下代码从配置中读取 HelloKey
,并在 /
终结点显示值。 如果配置值为 null,则“Hello”将分配给 message
:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
有关读取的配置源的完整列表,请参阅 ASP.NET Core 中的配置中的默认配置
添加日志记录提供程序
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
添加服务
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
自定义 IHostBuilder
可以使用 IHostBuilder访问 IHostBuilder 上的现有扩展方法:
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
自定义 IWebHostBuilder
可以使用 IWebHostBuilder 属性访问 IWebHostBuilder 上的扩展方法。
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
更改 Web 根
默认情况下,Web 根相对于 wwwroot
文件夹中的内容根。 Web 根是静态文件中间件查找静态文件的位置。 可以使用 WebHostOptions
、命令行或 UseWebRoot 方法更改 Web 根:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
自定义依赖项注入 (DI) 容器
下面的示例使用 Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
添加中间件
可以在 WebApplication
上配置任何现有的 ASP.NET Core 中间件:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
有关详细信息,请参阅 ASP.NET Core 中间件
开发人员异常页
WebApplication.CreateBuilder 使用预配置默认值初始化 WebApplicationBuilder 类的新实例。 开发人员异常页在预配置的默认值中启用。 当在开发环境中运行以下代码时,导航到 /
以呈现一个显示异常的友好页面。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ASP.NET Core 中间件
下表列出了一些经常与最小 API 一起使用的中间件。
中间件 | 描述 | API |
---|---|---|
身份验证 | 提供身份验证支持。 | UseAuthentication |
授权 | 提供身份验证支持。 | UseAuthorization |
CORS | 配置跨域资源共享。 | UseCors |
异常处理程序 | 全局处理中间件管道引发的异常。 | UseExceptionHandler |
转接头 | 将代理标头转发到当前请求。 | UseForwardedHeaders |
HTTPS 重定向 | 将所有 HTTP 请求重定向到 HTTPS。 | UseHttpsRedirection |
HTTP 严格传输安全性 (HSTS) | 添加特殊响应标头的安全增强中间件。 | UseHsts |
请求日志记录 | 提供对记录 HTTP 请求和响应的支持。 | UseHttpLogging |
请求超时 | 支持配置请求超时、全局默认值和每个终结点。 | UseRequestTimeouts |
W3C 请求日志记录 | 提供对以 W3C 格式记录 HTTP 请求和响应的支持。 | UseW3CLogging |
响应缓存 | 提供对缓存响应的支持。 | UseResponseCaching |
响应压缩 | 提供对压缩响应的支持。 | UseResponseCompression |
会话 | 提供对管理用户会话的支持。 | UseSession |
静态文件 | 为提供静态文件和目录浏览提供支持。 | UseStaticFiles、UseFileServer |
WebSockets | 启用 WebSockets 协议。 | UseWebSockets |
以下各部分介绍请求处理:路由、参数绑定和响应。
路由
配置的 WebApplication
支持 Map{Verb}
和 MapMethods,其中 {Verb}
是一种采用大小写混写格式的 HTTP 方法,如 Get
、Post
、Put
或 Delete
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
传递给这些方法的 Delegate 参数称为“路由处理程序”。
路由处理程序
路由处理程序是在路由匹配时执行的方法。 路由处理程序可以是 Lambda 表达式、本地函数、实例方法或静态方法。 路由处理程序可以是同步的,也可以是异步的。
Lambda 表达式
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
本地函数
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
实例方法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
静态方法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
在 Program.cs
外部定义的终结点
最小 API 不必位于 Program.cs
。
Program.cs
using MinAPISeparateFile;
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
TodoEndpoints.Map(app);
app.Run();
TodoEndpoints.cs
namespace MinAPISeparateFile;
public static class TodoEndpoints
{
public static void Map(WebApplication app)
{
app.MapGet("/", async context =>
{
// Get all todo items
await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
});
app.MapGet("/{id}", async context =>
{
// Get one todo item
await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
});
}
}
另请参阅本文后面的路由组。
命名终结点和链接生成
可以为终结点提供名称,以便为终结点生成 URL。 使用命名终结点可避免在应用中使用硬代码路径:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
前面的代码显示来自 /
终结点的 The link to the hello route is /hello
。
注意:终结点名称区分大小写。
终结点名称:
- 必须全局唯一。
- 在启用 OpenAPI 支持时用作 OpenAPI 操作 ID。 有关详细信息,请参阅 OpenAPI。
路由参数
路由参数可以作为路由模式定义的一部分进行捕获:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
前面的代码从 URI /users/3/books/7
返回 The user id is 3 and book id is 7
。
路由处理程序可以声明要捕获的参数。 当向带有声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,userId
和 bookId
均为 int
。
在前面的代码中,如果某个路由值无法转换为 int
,则会引发异常。 GET 请求 /users/hello/books/3
引发了以下异常:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
通配符和“全部捕获”路由
以下“全部捕获”路由从“/posts/hello”终结点返回 Routing to hello
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
路由约束
路由约束限制路由的匹配行为。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
下表显示了前面的路由模板及其行为:
路由模板 | 示例匹配 URI |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
有关详细信息,请参阅 ASP.NET Core 中的路由中的路由约束参考。
路由组
MapGroup 扩展方法有助于组织具有共同前缀的终结点组。 它减少了重复代码,并允许通过对添加终结点元数据的 RequireAuthorization 和 WithMetadata 等方法的单一调用来自定义整个终结点组。
例如,以下代码创建两组相似的终结点:
app.MapGroup("/public/todos")
.MapTodosApi()
.WithTags("Public");
app.MapGroup("/private/todos")
.MapTodosApi()
.WithTags("Private")
.AddEndpointFilterFactory(QueryPrivateTodos)
.RequireAuthorization();
EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
var dbContextIndex = -1;
foreach (var argument in factoryContext.MethodInfo.GetParameters())
{
if (argument.ParameterType == typeof(TodoDb))
{
dbContextIndex = argument.Position;
break;
}
}
// Skip filter if the method doesn't have a TodoDb parameter.
if (dbContextIndex < 0)
{
return next;
}
return async invocationContext =>
{
var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
dbContext.IsPrivate = true;
try
{
return await next(invocationContext);
}
finally
{
// This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
dbContext.IsPrivate = false;
}
};
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
group.MapGet("/", GetAllTodos);
group.MapGet("/{id}", GetTodo);
group.MapPost("/", CreateTodo);
group.MapPut("/{id}", UpdateTodo);
group.MapDelete("/{id}", DeleteTodo);
return group;
}
在此方案中,可以适用 201 Created
结果中 Location
标头的相对地址:
public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
await database.AddAsync(todo);
await database.SaveChangesAsync();
return TypedResults.Created($"{todo.Id}", todo);
}
第一组终结点将仅匹配前缀为 /public/todos
并且无需任何身份验证即可访问的请求。 第二组终结点将仅匹配前缀为 /private/todos
并且需要身份验证的请求。
QueryPrivateTodos
终结点筛选器工厂是一种本地函数,用于修改路由处理程序的 TodoDb
参数,以允许访问和存储专用的 todo 数据。
路由组还支持嵌套组和具有路由参数和约束的复杂前缀模式。 在以下示例中,映射到 user
组的路由处理程序可以捕获外部组前缀中定义的 {org}
和 {group}
路由参数。
前缀也可以为空。 这对于在不更改路由模式的情况下向一组终结点添加终结点元数据或筛选器非常有用。
var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");
向组添加筛选器或元数据的行为与在添加可能已添加到内部组或特定终结点的任何额外筛选器或元数据之前将它们单独添加到每个终结点的行为相同。
var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");
inner.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/inner group filter");
return next(context);
});
outer.AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("/outer group filter");
return next(context);
});
inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
app.Logger.LogInformation("MapGet filter");
return next(context);
});
在上面的示例中,外部筛选器将在内部筛选器之前记录传入请求,即使它是第二个添加的。 由于筛选器应用于不同的组,因此它们相对于彼此的添加顺序并不重要。 如果应用于相同的组或特定终结点,则筛选器的添加顺序非常重要。
对 /outer/inner/
的请求将记录以下内容:
/outer group filter
/inner group filter
MapGet filter
参数绑定
参数绑定是将请求数据转换为由路由处理程序表示的强类型参数的过程。 绑定源确定绑定参数的位置。 绑定源可以是显式的,也可以是基于 HTTP 方法和参数类型推断的。
支持的绑定源:
- 路由值
- 查询字符串
- 头文件
- 正文(作为 JSON)
- 依赖项注入提供的服务
- 自定义
.NET 6 和 7 本身不支持从窗体值进行绑定。
下面的 GET
路由处理程序使用其中一些参数绑定源:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
下表显示了前面的示例中使用的参数与关联的绑定源之间的关系。
参数 | 绑定源 |
---|---|
id |
路由值 |
page |
查询字符串 |
customHeader |
标头的值开始缓存响应 |
service |
由依赖项注入提供 |
HTTP 方法 GET
、HEAD
、OPTIONS
和 DELETE
不会从正文隐式绑定。 如需从这些 HTTP 方法的主体(如 JSON)进行绑定,可以 使用 [FromBody]
显式绑定或从 HttpRequest 读取。
以下示例 POST 路由处理程序将主体(如 JSON)的绑定源用于 person
参数:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
上述示例中的参数都自动从请求数据绑定。 为了演示参数绑定的便利性,以下路由处理程序演示如何直接从请求读取请求数据:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
显式参数绑定
特性可用于显式声明绑定参数的位置。
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" 的标头 |
注意
.NET 6 和 7 本身不支持从窗体值进行绑定。
通过依赖关系注入进行参数绑定
当类型配置为服务时,最小 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();
可选参数
在路由处理程序中声明的参数被视为必需参数:
- 如果请求与路由匹配,则只有在请求中提供了所有必需的参数时,路由处理程序才会运行。
- 未能提供所有必需的参数会导致错误。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
BadHttpRequestException :查询字符串中未提供必需的参数“int pageNumber”。 |
/products/1 |
HTTP 404 错误,无匹配的路由 |
若要设置为 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 | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products2 |
已返回 1 |
前面的可为空默认值适用于所有源:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
如果未发送请求正文,则前面的代码将使用 null 产品调用方法。
注意:如果提供的数据无效并且参数可为空,则路由处理程序不运行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products?pageNumber=two |
BadHttpRequestException :无法从“two”绑定参数 "Nullable<int> pageNumber" 。 |
/products/two |
HTTP 404 错误,无匹配的路由 |
有关详细信息,请参阅绑定失败部分。
特殊类型
以下类型在绑定时没有显式特性:
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"]}"));
CancellationToken:与当前 HTTP 请求关联的取消标记:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal:与请求关联的用户,从 HttpContext.User 进行绑定:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
将请求正文绑定为 Stream
或 PipeReader
请求正文可以绑定为 Stream
或 PipeReader
,以有效支持用户必须处理数据的情况,以及:
- 将数据存储在 Blob 存储中,或将数据排入队列提供程序的队列。
- 使用工作进程或云功能处理存储的数据。
例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。
以下代码可实现后台队列:
using System.Text.Json;
using System.Threading.Channels;
namespace BackgroundQueueService;
class BackgroundQueue : BackgroundService
{
private readonly Channel<ReadOnlyMemory<byte>> _queue;
private readonly ILogger<BackgroundQueue> _logger;
public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
ILogger<BackgroundQueue> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
{
try
{
var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
_logger.LogInformation($"{person.Name} is {person.Age} " +
$"years and from {person.Country}");
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
}
}
class Person
{
public string Name { get; set; } = String.Empty;
public int Age { get; set; }
public string Country { get; set; } = String.Empty;
}
以下代码将请求正文绑定到 Stream
:
app.MapPost("/register", async (HttpRequest req, Stream body,
Channel<ReadOnlyMemory<byte>> queue) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
以下代码显示完整的 Program.cs
文件:
using System.Threading.Channels;
using BackgroundQueueService;
var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;
// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;
// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;
// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));
// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();
// 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) =>
{
if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
{
return Results.BadRequest();
}
// We're not above the message size and we have a content length, or
// we're a chunked request and we're going to read up to the maxMessageSize + 1.
// We add one to the message size so that we can detect when a chunked request body
// is bigger than our configured max.
var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);
var buffer = new byte[readSize];
// Read at least that many bytes from the body.
var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);
// We read more than the max, so this is a bad request.
if (read > maxMessageSize)
{
return Results.BadRequest();
}
// Attempt to send the buffer to the background queue.
if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
{
return Results.Accepted();
}
// We couldn't accept the message since we're overloaded.
return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});
app.Run();
- 读取数据时,
Stream
是与HttpRequest.Body
相同的对象。 - 默认情况下,不缓冲请求正文。 读取正文后,不支持后退。 无法多次读取流。
- 不能在最小操作处理程序之外使用
Stream
和PipeReader
,因为基础缓冲区将被释放或重用。
使用 IFormFile 和 IFormFileCollection 上传文件
以下代码使用 IFormFile 和 IFormFileCollection 上传文件:
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();
使用授权标头、客户端证书或 cookie 标头支持经过身份验证的文件上传请求。
ASP.NET Core 7.0 中未提供对防伪的内置支持。 防伪在 ASP.NET Core 8.0 及更高版本中可用。 但是,这可以使用 IAntiforgery
服务来实现。
绑定标头和查询字符串中的数组和字符串值
下面的代码展示如何将查询字符串绑定到基元类型、字符串数组和 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]}");
在类型实现 TryParse
时,支持将查询字符串或标头值绑定到复杂类型的数组。 以下代码绑定到字符串数组,并返回具有指定标记的所有项:
// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
return await db.Todos
.Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
.ToListAsync();
});
以下代码显示了模型和所需的 TryParse
实现:
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
// This is an owned entity.
public Tag Tag { get; set; } = new();
}
[Owned]
public class Tag
{
public string? Name { get; set; } = "n/a";
public static bool TryParse(string? name, out Tag tag)
{
if (name is null)
{
tag = default!;
return false;
}
tag = new Tag { Name = name };
return true;
}
}
以下代码绑定到 int
数组:
// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
return await db.Todos
.Where(t => ids.Contains(t.Id))
.ToListAsync();
});
若要测试上述代码,请添加以下终结点以使用 Todo
项填充数据库:
// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
await db.Todos.AddRangeAsync(todos);
await db.SaveChangesAsync();
return Results.Ok(todos);
});
使用 HttpRepl
之类的 API 测试工具将以下数据传递给上一终结点:
[
{
"id": 1,
"name": "Have Breakfast",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 2,
"name": "Have Lunch",
"isComplete": true,
"tag": {
"name": "work"
}
},
{
"id": 3,
"name": "Have Supper",
"isComplete": true,
"tag": {
"name": "home"
}
},
{
"id": 4,
"name": "Have Snacks",
"isComplete": true,
"tag": {
"name": "N/A"
}
}
]
以下代码绑定到标头键 X-Todo-Id
,并返回具有匹配 Id
值的 Todo
项:
// 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();
});
注意
从查询字符串绑定 string[]
时,缺少任何匹配的查询字符串值都会导致空数组而不是 null 值。
使用 [AsParameters] 对参数列表进行参数绑定
AsParametersAttribute 启用对类型的简单参数绑定而不是复杂或递归模型绑定。
考虑下列代码:
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());
app.MapGet("/todoitems/{id}",
async (int Id, TodoDb Db) =>
await Db.Todos.FindAsync(Id)
is Todo todo
? Results.Ok(new TodoItemDTO(todo))
: Results.NotFound());
// Remaining code removed for brevity.
请考虑以下 GET
终结点:
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
可用于替换上述突出显示的参数:
struct TodoItemRequest
{
public int Id { get; set; }
public TodoDb Db { get; set; }
}
重构的 GET
终结点将上述 struct
与 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());
以下代码显示应用中的其他终结点:
app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
var todoItem = new Todo
{
IsComplete = Dto.IsComplete,
Name = Dto.Name
};
Db.Todos.Add(todoItem);
await Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
var todo = await Db.Todos.FindAsync(Id);
if (todo is null) return Results.NotFound();
todo.Name = Dto.Name;
todo.IsComplete = Dto.IsComplete;
await Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
if (await Db.Todos.FindAsync(Id) is Todo todo)
{
Db.Todos.Remove(todo);
await Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
以下类用于重构参数列表:
class CreateTodoItemRequest
{
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
class EditTodoItemRequest
{
public int Id { get; set; }
public TodoItemDTO Dto { get; set; } = default!;
public TodoDb Db { get; set; } = default!;
}
以下代码显示了使用 AsParameters
和上述 struct
及类的重构终结点:
app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
var todoItem = new Todo
{
IsComplete = request.Dto.IsComplete,
Name = request.Dto.Name
};
request.Db.Todos.Add(todoItem);
await request.Db.SaveChangesAsync();
return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});
app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
var todo = await request.Db.Todos.FindAsync(request.Id);
if (todo is null) return Results.NotFound();
todo.Name = request.Dto.Name;
todo.IsComplete = request.Dto.IsComplete;
await request.Db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
{
request.Db.Todos.Remove(todo);
await request.Db.SaveChangesAsync();
return Results.Ok(new TodoItemDTO(todo));
}
return Results.NotFound();
});
以下 record
类型可用于替换上述参数:
record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);
将 struct
和 AsParameters
一起使用可能比使用 record
类型性能更佳。
AspNetCore.Docs.Samples 存储库中的完整示例代码。
自定义绑定
自定义参数绑定有两种方法:
- 对于路由、查询和标头绑定源,通过添加类型的静态
TryParse
方法来绑定自定义类型。 - 通过对类型实现
BindAsync
方法来控制绑定过程。
TryParse
TryParse
具有两个 API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
下面的代码显示带有 URI /map?Point=12.3,10.1
的 Point: 12.3, 10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
具有以下 API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
下面的代码显示带有 URI /products?SortBy=xyz&SortDir=Desc&Page=99
的 SortBy:xyz, SortDirection:Desc, CurrentPage:99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
绑定失败
绑定失败时,框架会记录调试消息,并根据失败模式将各种状态代码返回到客户端。
故障模式 | 可为空参数类型 | 绑定源 | 状态代码 |
---|---|---|---|
{ParameterType}.TryParse 返回 false |
是 | route/query/header | 400 |
{ParameterType}.BindAsync 返回 null |
是 | 自定义 | 400 |
{ParameterType}.BindAsync 引发 |
不重要 | 自定义 | 500 |
未能反序列化 JSON 正文 | 不重要 | body | 400 |
错误的内容类型(不是 application/json ) |
不重要 | body | 415 |
绑定优先级
用于从参数确定绑定源的规则:
- 按以下顺序在参数(From* 属性)上定义的显式属性:
- 路由值:
[FromRoute]
- 查询字符串:
[FromQuery]
- 标头:
[FromHeader]
- 正文:
[FromBody]
- 一个服务:
[FromServices]
- 参数值:
[AsParameters]
- 路由值:
- 特殊类型
HttpContext
HttpRequest
(HttpContext.Request
)HttpResponse
(HttpContext.Response
)ClaimsPrincipal
(HttpContext.User
)CancellationToken
(HttpContext.RequestAborted
)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) => {});
中,id
从路由中绑定。 - 从查询字符串进行绑定。
- 如果路由模板中存在参数名称。 在
- 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
- 参数来自正文。
为正文绑定配置 JSON 反序列化选项
正文绑定源使用 System.Text.Json 进行反序列化。 不能更改此默认值,但可以配置 JSON 序列化和反序列化选项。
全局配置 JSON 反序列化选项
全局应用于应用的选项可以通过调用 ConfigureHttpJsonOptions 进行配置。 以下示例包含公共字段,并设置 JSON 输出的格式。
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
options.SerializerOptions.WriteIndented = true;
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/", (Todo todo) => {
if (todo is not null) {
todo.Name = todo.NameField;
}
return todo;
});
app.Run();
class Todo {
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "nameField":"Walk dog",
// "isComplete":false
// }
由于示例代码同时配置序列化和反序列化,因此它可以读取 NameField
并在输出 JSON 中包含 NameField
。
为终结点配置 JSON 反序列化选项
ReadFromJsonAsync 具有接受 JsonSerializerOptions 对象的重载。 以下示例包含公共字段,并设置 JSON 输出的格式。
using System.Text.Json;
var app = WebApplication.Create();
var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) {
IncludeFields = true,
WriteIndented = true
};
app.MapPost("/", async (HttpContext context) => {
if (context.Request.HasJsonContentType()) {
var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
if (todo is not null) {
todo.Name = todo.NameField;
}
return Results.Ok(todo);
}
else {
return Results.BadRequest();
}
});
app.Run();
class Todo
{
public string? Name { get; set; }
public string? NameField;
public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
// "name":"Walk dog",
// "isComplete":false
// }
由于上述代码仅将自定义选项应用于反序列化,因此输出 JSON 不包括 NameField
。
读取请求正文
使用 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();
前面的代码:
- 使用 HttpRequest.BodyReader 访问请求正文。
- 将请求正文复制到本地文件。
响应
路由处理程序支持以下类型的返回值:
- 基于
IResult
- 这包括Task<IResult>
和ValueTask<IResult>
ValueTask<string>
- 这包括string
和Task<string>
T
(任何其他类型)- 这包括Task<T>
和ValueTask<T>
返回值 | 行为 | Content-Type |
---|---|---|
IResult |
框架调用 IResult.ExecuteAsync | 由 IResult 实现决定 |
string |
框架将字符串直接写入响应 | text/plain |
T (任何其他类型) |
框架将对响应进行 JSON 序列化 | application/json |
有关路由处理程序返回值的更深入指南,请参阅在最小 API 应用程序中创建响应
示例返回值
字符串返回值
app.MapGet("/hello", () => "Hello World");
JSON 返回值
app.MapGet("/hello", () => new { Message = "Hello World" });
返回 TypedResults
以下代码将返回 TypedResults:
app.MapGet("/hello", () => TypedResults.Ok(new Message() { Text = "Hello World!" }));
优先返回 TypedResults
,而不是 Results。 有关详细信息,请参阅 TypedResults 与 Results。
IResult 返回值
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
以下示例使用内置结果类型自定义响应:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
自定义状态代码
app.MapGet("/405", () => Results.StatusCode(405));
文本
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
有关更多示例,请参阅在最小 API 应用程序中创建响应。
重定向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
文件
app.MapGet("/download", () => Results.File("myfile.text"));
内置结果
Results 和 TypedResults 静态类中存在常见的结果帮助程序。 优先返回 TypedResults
,而不是 Results
。 有关详细信息,请参阅 TypedResults 与 Results。
自定义结果
应用程序可以通过实现自定义 IResult 类型来控制响应。 以下代码是 HTML 结果类型的示例:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
建议将扩展方法添加到 Microsoft.AspNetCore.Http.IResultExtensions,使这些自定义结果更容易发现。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
类型化结果
IResult 接口可以表示从最小 API 返回的值,这些 API 不利用对 JSON 将返回的对象序列化为 HTTP 响应的隐式支持。 静态 Results 类用于创建各种 IResult
对象,这些对象表示不同类型的响应。 例如,设置响应状态代码或重定向到另一个 URL。
实现 IResult
的类型是公共类型,允许在测试时使用类型断言。 例如:
[TestClass()]
public class WeatherApiTests
{
[TestMethod()]
public void MapWeatherApiTest()
{
var result = WeatherApi.GetAllWeathers();
Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
}
}
可以查看静态 TypedResults 类上相应方法的返回类型,以找到要转换为的正确公共 IResult
类型。
有关更多示例,请参阅在最小 API 应用程序中创建响应。
筛选器
请参阅最小 API 筛选器应用
授权
可以使用授权策略保护路由。 可以通过 [Authorize]
属性或通过使用 RequireAuthorization 方法声明这些对象:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
前面的代码可以用 RequireAuthorization 编写:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
以下示例使用基于策略的授权:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
允许未经身份验证的用户访问终结点
[AllowAnonymous]
允许未经身份验证的用户访问终结点:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
路由可以使用 CORS 策略启用 CORS。 可以通过 [EnableCors]
属性或通过使用 RequireCors 方法声明 CORS。 以下示例启用 CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS)
另请参阅
此文档:
- 提供有关最小 API 的快速参考。
- 适用于经验丰富的开发人员。 有关说明,请参阅教程:使用 ASP.NET Core 创建最小 API
最小的 API 包括:
- WebApplication 和 WebApplicationBuilder
- 路由处理程序
WebApplication
以下代码由 ASP.NET Core 模板生成:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
可以通过命令行上的 dotnet new web
或在 Visual Studio 中选择“空 Web”模板来创建前面的代码。
以下代码创建 WebApplication (app
),而无需显式创建 WebApplicationBuilder:
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run();
WebApplication.Create
使用预配置默认值初始化 WebApplication 类的新实例。
使用端口
使用 Visual Studio 或 dotnet new
创建 Web 应用时,将创建 Properties/launchSettings.json
文件,该文件指定应用响应的端口。 在后续的端口设置示例中,从 Visual Studio 运行应用会返回错误对话框 Unable to connect to web server 'AppName'
。 从命令行运行以下端口更改示例。
以下部分设置应用响应的端口。
var app = WebApplication.Create(args);
app.MapGet("/", () => "Hello World!");
app.Run("http://localhost:3000");
在前面的代码中,应用响应端口 3000
。
多个端口
在以下代码中,应用响应端口 3000
和 4000
。
var app = WebApplication.Create(args);
app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");
app.MapGet("/", () => "Hello World");
app.Run();
从命令行设置端口
以下命令使应用响应端口 7777
:
dotnet run --urls="https://localhost:7777"
如果在 appsettings.json
文件中也配置了 Kestrel 终结点,则使用 appsettings.json
文件指定的 URL。 有关详细信息,请参阅Kestrel终结点配置
从环境中读取端口
以下代码从环境中读取端口:
var app = WebApplication.Create(args);
var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";
app.MapGet("/", () => "Hello World");
app.Run($"http://localhost:{port}");
从环境设置端口的首选方法为使用 ASPNETCORE_URLS
环境变量,如以下部分所示。
通过 ASPNETCORE_URLS 环境变量设置端口
ASPNETCORE_URLS
环境变量可用于设置端口:
ASPNETCORE_URLS=http://localhost:3000
ASPNETCORE_URLS
支持多个 URL:
ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000
侦听所有接口
以下示例演示如何侦听所有接口
http://*:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://*:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://+:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://+:3000");
app.MapGet("/", () => "Hello World");
app.Run();
http://0.0.0.0:3000
var app = WebApplication.Create(args);
app.Urls.Add("http://0.0.0.0:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用 ASPNETCORE_URLS 侦听所有接口
前面的示例可以使用 ASPNETCORE_URLS
ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005
指定使用开发证书的 HTTPS
var app = WebApplication.Create(args);
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
有关开发证书详细信息,请参阅在 Windows 和 macOS 上信任 ASP.NET Core HTTPS 开发证书。
指定使用自定义证书的 HTTPS
以下部分显示如何使用 appsettings.json
文件和通过配置指定自定义证书。
使用 appsettings.json
指定自定义证书
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"Certificates": {
"Default": {
"Path": "cert.pem",
"KeyPath": "key.pem"
}
}
}
}
通过配置指定自定义证书
var builder = WebApplication.CreateBuilder(args);
// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
使用证书 API
using System.Security.Cryptography.X509Certificates;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(httpsOptions =>
{
var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");
httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath,
keyPath);
});
});
var app = builder.Build();
app.Urls.Add("https://localhost:3000");
app.MapGet("/", () => "Hello World");
app.Run();
读取环境
var app = WebApplication.Create(args);
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/oops");
}
app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");
app.Run();
有关使用环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境
配置
以下代码从配置系统读取:
var app = WebApplication.Create(args);
var message = app.Configuration["HelloKey"] ?? "Hello";
app.MapGet("/", () => message);
app.Run();
有关详细信息,请参阅 ASP.NET Core 中的配置
Logging
以下代码在应用程序启动时将消息写入日志:
var app = WebApplication.Create(args);
app.Logger.LogInformation("The app started");
app.MapGet("/", () => "Hello World");
app.Run();
有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录
访问依赖项注入 (DI) 容器
下面的代码演示如何在应用程序启动过程中从 DI 容器获取服务:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();
var app = builder.Build();
app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
sampleService.DoSomething();
}
app.Run();
有关详细信息,请参阅 ASP.NET Core 中的依赖项注入。
WebApplicationBuilder
本部分包含使用 WebApplicationBuilder 的示例代码。
更改内容根、应用程序名称和环境
以下代码设置内容根、应用程序名称和环境:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
ApplicationName = typeof(Program).Assembly.FullName,
ContentRootPath = Directory.GetCurrentDirectory(),
EnvironmentName = Environments.Staging,
WebRootPath = "customwwwroot"
});
Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");
var app = builder.Build();
WebApplication.CreateBuilder 使用预配置的默认值初始化 WebApplicationBuilder 类的新实例。
有关详细信息,请参阅 ASP.NET Core 基础知识概述
按环境变量或命令行更改内容根、应用程序名称和环境
下表显示了用于更改内容根、应用程序名称和环境的环境变量及命令行参数:
feature | 环境变量 | 命令行参数 |
---|---|---|
应用程序名称 | ASPNETCORE_APPLICATIONNAME | --applicationName |
环境名称 | ASPNETCORE_ENVIRONMENT | --environment |
内容根 | ASPNETCORE_CONTENTROOT | --contentRoot |
添加配置提供程序
以下示例添加 INI 配置提供程序:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddIniFile("appsettings.ini");
var app = builder.Build();
有关详细信息,请参阅 ASP.NET Core 中的配置中的文件配置提供程序。
读取配置
默认情况下,WebApplicationBuilder 从多个源读取配置,包括:
appSettings.json
和appSettings.{environment}.json
- 环境变量
- 命令行
有关读取的配置源的完整列表,请参阅 ASP.NET Core 中的配置中的默认配置
以下代码从配置中读取 HelloKey
,并在 /
终结点显示值。 如果配置值为 null,则“Hello”将分配给 message
:
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
读取环境
var builder = WebApplication.CreateBuilder(args);
var message = builder.Configuration["HelloKey"] ?? "Hello";
var app = builder.Build();
app.MapGet("/", () => message);
app.Run();
添加日志记录提供程序
var builder = WebApplication.CreateBuilder(args);
// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();
var app = builder.Build();
app.MapGet("/", () => "Hello JSON console!");
app.Run();
添加服务
var builder = WebApplication.CreateBuilder(args);
// Add the memory cache services.
builder.Services.AddMemoryCache();
// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();
自定义 IHostBuilder
可以使用 IHostBuilder访问 IHostBuilder 上的现有扩展方法:
var builder = WebApplication.CreateBuilder(args);
// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
自定义 IWebHostBuilder
可以使用 IWebHostBuilder 属性访问 IWebHostBuilder 上的扩展方法。
var builder = WebApplication.CreateBuilder(args);
// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();
var app = builder.Build();
app.MapGet("/", () => "Hello HTTP.sys");
app.Run();
更改 Web 根
默认情况下,Web 根相对于 wwwroot
文件夹中的内容根。 Web 根是静态文件中间件查找静态文件的位置。 可以使用 WebHostOptions
、命令行或 UseWebRoot 方法更改 Web 根:
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
Args = args,
// Look for static files in webroot
WebRootPath = "webroot"
});
var app = builder.Build();
app.Run();
自定义依赖项注入 (DI) 容器
下面的示例使用 Autofac:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));
var app = builder.Build();
添加中间件
可以在 WebApplication
上配置任何现有的 ASP.NET Core 中间件:
var app = WebApplication.Create(args);
// Setup the file server to serve static files.
app.UseFileServer();
app.MapGet("/", () => "Hello World!");
app.Run();
有关详细信息,请参阅 ASP.NET Core 中间件
开发人员异常页
WebApplication.CreateBuilder 使用预配置默认值初始化 WebApplicationBuilder 类的新实例。 开发人员异常页在预配置的默认值中启用。 当在开发环境中运行以下代码时,导航到 /
以呈现一个显示异常的友好页面。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () =>
{
throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});
app.Run();
ASP.NET Core 中间件
下表列出了一些经常与最小 API 一起使用的中间件。
中间件 | 描述 | API |
---|---|---|
身份验证 | 提供身份验证支持。 | UseAuthentication |
授权 | 提供身份验证支持。 | UseAuthorization |
CORS | 配置跨域资源共享。 | UseCors |
异常处理程序 | 全局处理中间件管道引发的异常。 | UseExceptionHandler |
转接头 | 将代理标头转发到当前请求。 | UseForwardedHeaders |
HTTPS 重定向 | 将所有 HTTP 请求重定向到 HTTPS。 | UseHttpsRedirection |
HTTP 严格传输安全性 (HSTS) | 添加特殊响应标头的安全增强中间件。 | UseHsts |
请求日志记录 | 提供对记录 HTTP 请求和响应的支持。 | UseHttpLogging |
W3C 请求日志记录 | 提供对以 W3C 格式记录 HTTP 请求和响应的支持。 | UseW3CLogging |
响应缓存 | 提供对缓存响应的支持。 | UseResponseCaching |
响应压缩 | 提供对压缩响应的支持。 | UseResponseCompression |
会话 | 提供对管理用户会话的支持。 | UseSession |
静态文件 | 为提供静态文件和目录浏览提供支持。 | UseStaticFiles、UseFileServer |
WebSockets | 启用 WebSockets 协议。 | UseWebSockets |
请求处理
以下各部分介绍路由、参数绑定和响应。
路由
配置的 WebApplication
支持 Map{Verb}
和 MapMethods:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");
app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" },
() => "This is an options or head request ");
app.Run();
路由处理程序
路由处理程序是在路由匹配时执行的方法。 路由处理程序可以是任何形式的函数,包括同步或异步。 路由处理程序可以是 Lambda 表达式、本地函数、实例方法或静态方法。
Lambda 表达式
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/inline", () => "This is an inline lambda");
var handler = () => "This is a lambda variable";
app.MapGet("/", handler);
app.Run();
本地函数
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
string LocalFunction() => "This is local function";
app.MapGet("/", LocalFunction);
app.Run();
实例方法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var handler = new HelloHandler();
app.MapGet("/", handler.Hello);
app.Run();
class HelloHandler
{
public string Hello()
{
return "Hello Instance method";
}
}
静态方法
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", HelloHandler.Hello);
app.Run();
class HelloHandler
{
public static string Hello()
{
return "Hello static method";
}
}
命名终结点和链接生成
可以为终结点提供名称,以便为终结点生成 URL。 使用命名终结点可避免在应用中使用硬代码路径:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello", () => "Hello named route")
.WithName("hi");
app.MapGet("/", (LinkGenerator linker) =>
$"The link to the hello route is {linker.GetPathByName("hi", values: null)}");
app.Run();
前面的代码显示来自 /
终结点的 The link to the hello endpoint is /hello
。
注意:终结点名称区分大小写。
终结点名称:
- 必须全局唯一。
- 在启用 OpenAPI 支持时用作 OpenAPI 操作 ID。 有关详细信息,请参阅 OpenAPI。
路由参数
路由参数可以作为路由模式定义的一部分进行捕获:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/users/{userId}/books/{bookId}",
(int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");
app.Run();
前面的代码从 URI /users/3/books/7
返回 The user id is 3 and book id is 7
。
路由处理程序可以声明要捕获的参数。 当使用声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,userId
和 bookId
均为 int
。
在前面的代码中,如果某个路由值无法转换为 int
,则会引发异常。 GET 请求 /users/hello/books/3
引发了以下异常:
BadHttpRequestException: Failed to bind parameter "int userId" from "hello".
通配符和“全部捕获”路由
以下“全部捕获”路由从“/posts/hello”终结点返回 Routing to hello
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");
app.Run();
路由约束
路由约束限制路由的匹配行为。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");
app.Run();
下表显示了前面的路由模板及其行为:
路由模板 | 示例匹配 URI |
---|---|
/todos/{id:int} |
/todos/1 |
/todos/{text} |
/todos/something |
/posts/{slug:regex(^[a-z0-9_-]+$)} |
/posts/mypost |
有关详细信息,请参阅 ASP.NET Core 中的路由中的路由约束参考。
参数绑定
参数绑定是将请求数据转换为由路由处理程序表示的强类型参数的过程。 绑定源确定绑定参数的位置。 绑定源可以是显式的,也可以是基于 HTTP 方法和参数类型推断的。
支持的绑定源:
- 路由值
- 查询字符串
- 头文件
- 正文(作为 JSON)
- 依赖项注入提供的服务
- 自定义
注意
.NET 不支持从窗体值进行绑定。
下面的示例 GET 路由处理程序使用其中一些参数绑定源:
var builder = WebApplication.CreateBuilder(args);
// Added as service
builder.Services.AddSingleton<Service>();
var app = builder.Build();
app.MapGet("/{id}", (int id,
int page,
[FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
Service service) => { });
class Service { }
下表显示了前面的示例中使用的参数与关联的绑定源之间的关系。
参数 | 绑定源 |
---|---|
id |
路由值 |
page |
查询字符串 |
customHeader |
标头的值开始缓存响应 |
service |
由依赖项注入提供 |
HTTP 方法 GET
、HEAD
、OPTIONS
和 DELETE
不会从正文隐式绑定。 如需从这些 HTTP 方法的主体(如 JSON)进行绑定,可以 使用 [FromBody]
显式绑定或从 HttpRequest 读取。
以下示例 POST 路由处理程序将主体(如 JSON)的绑定源用于 person
参数:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/", (Person person) => { });
record Person(string Name, int Age);
上述示例中的参数都自动从请求数据绑定。 为了演示参数绑定的便利性,以下示例路由处理程序演示如何直接从请求读取请求数据:
app.MapGet("/{id}", (HttpRequest request) =>
{
var id = request.RouteValues["id"];
var page = request.Query["page"];
var customHeader = request.Headers["X-CUSTOM-HEADER"];
// ...
});
app.MapPost("/", async (HttpRequest request) =>
{
var person = await request.ReadFromJsonAsync<Person>();
// ...
});
显式参数绑定
特性可用于显式声明绑定参数的位置。
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" 的标头 |
注意
.NET 不支持从窗体值进行绑定。
与 DI 的参数绑定
当类型配置为服务时,最小 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();
可选参数
在路由处理程序中声明的参数被视为必需参数:
- 如果请求与路由匹配,则只有在请求中提供了所有必需的参数时,路由处理程序才会运行。
- 未能提供所有必需的参数会导致错误。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
BadHttpRequestException :查询字符串中未提供必需的参数“int pageNumber”。 |
/products/1 |
HTTP 404 错误,无匹配的路由 |
若要设置为 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 | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products2 |
已返回 1 |
前面的可为空默认值适用于所有源:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/products", (Product? product) => { });
app.Run();
如果未发送请求正文,则前面的代码将使用 null 产品调用方法。
注意:如果提供的数据无效并且参数可为空,则路由处理程序不运行。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");
app.Run();
URI | result |
---|---|
/products?pageNumber=3 |
已返回 3 |
/products |
已返回 1 |
/products?pageNumber=two |
BadHttpRequestException :无法从“two”绑定参数 "Nullable<int> pageNumber" 。 |
/products/two |
HTTP 404 错误,无匹配的路由 |
有关详细信息,请参阅绑定失败部分。
特殊类型
以下类型在绑定时没有显式特性:
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"]}"));
CancellationToken:与当前 HTTP 请求关联的取消标记:
app.MapGet("/", async (CancellationToken cancellationToken) => await MakeLongRunningRequestAsync(cancellationToken));
ClaimsPrincipal:与请求关联的用户,从 HttpContext.User 进行绑定:
app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
自定义绑定
自定义参数绑定有两种方法:
- 对于路由、查询和标头绑定源,通过添加类型的静态
TryParse
方法来绑定自定义类型。 - 通过对类型实现
BindAsync
方法来控制绑定过程。
TryParse
TryParse
具有两个 API:
public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);
下面的代码显示带有 URI /map?Point=12.3,10.1
的 Point: 12.3, 10.1
:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");
app.Run();
public class Point
{
public double X { get; set; }
public double Y { get; set; }
public static bool TryParse(string? value, IFormatProvider? provider,
out Point? point)
{
// Format is "(12.3,10.1)"
var trimmedValue = value?.TrimStart('(').TrimEnd(')');
var segments = trimmedValue?.Split(',',
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (segments?.Length == 2
&& double.TryParse(segments[0], out var x)
&& double.TryParse(segments[1], out var y))
{
point = new Point { X = x, Y = y };
return true;
}
point = null;
return false;
}
}
BindAsync
BindAsync
具有以下 API:
public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);
下面的代码显示带有 URI /products?SortBy=xyz&SortDir=Desc&Page=99
的 SortBy:xyz, SortDirection:Desc, CurrentPage:99
:
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
$"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");
app.Run();
public class PagingData
{
public string? SortBy { get; init; }
public SortDirection SortDirection { get; init; }
public int CurrentPage { get; init; } = 1;
public static ValueTask<PagingData?> BindAsync(HttpContext context,
ParameterInfo parameter)
{
const string sortByKey = "sortBy";
const string sortDirectionKey = "sortDir";
const string currentPageKey = "page";
Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
ignoreCase: true, out var sortDirection);
int.TryParse(context.Request.Query[currentPageKey], out var page);
page = page == 0 ? 1 : page;
var result = new PagingData
{
SortBy = context.Request.Query[sortByKey],
SortDirection = sortDirection,
CurrentPage = page
};
return ValueTask.FromResult<PagingData?>(result);
}
}
public enum SortDirection
{
Default,
Asc,
Desc
}
绑定失败
绑定失败时,框架会记录调试消息,并根据失败模式将各种状态代码返回到客户端。
故障模式 | 可为空参数类型 | 绑定源 | 状态代码 |
---|---|---|---|
{ParameterType}.TryParse 返回 false |
是 | route/query/header | 400 |
{ParameterType}.BindAsync 返回 null |
是 | 自定义 | 400 |
{ParameterType}.BindAsync 引发 |
不重要 | 自定义 | 500 |
未能反序列化 JSON 正文 | 不重要 | body | 400 |
错误的内容类型(不是 application/json ) |
不重要 | body | 415 |
绑定优先级
用于从参数确定绑定源的规则:
- 按以下顺序在参数(From* 属性)上定义的显式属性:
- 路由值:
[FromRoute]
- 查询字符串:
[FromQuery]
- 标头:
[FromHeader]
- 正文:
[FromBody]
- 服务:
[FromServices]
- 路由值:
- 特殊类型
- 参数类型具有有效的
BindAsync
方法。 - 参数类型为字符串或具有有效的
TryParse
方法。- 如果路由模板中存在参数名称。 在
app.Map("/todo/{id}", (int id) => {});
中,id
从路由中绑定。 - 从查询字符串进行绑定。
- 如果路由模板中存在参数名称。 在
- 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
- 参数来自正文。
自定义 JSON 绑定
正文绑定源使用 System.Text.Json 进行反序列化。 不能更改此默认值,但可以使用前面所述的其他方法自定义绑定。 若要自定义 JSON 序列化程序选项,请使用类似于下面的代码:
using Microsoft.AspNetCore.Http.Json;
var builder = WebApplication.CreateBuilder(args);
// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.IncludeFields = true;
});
var app = builder.Build();
app.MapPost("/products", (Product product) => product);
app.Run();
class Product
{
// These are public fields, not properties.
public int Id;
public string? Name;
}
前面的代码:
- 配置输入和输出默认 JSON 选项。
- 返回以下 JSON
发布时{ "id": 1, "name": "Joe Smith" }
{ "Id": 1, "Name": "Joe Smith" }
读取请求正文
使用 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();
前面的代码:
- 使用 HttpRequest.BodyReader 访问请求正文。
- 将请求正文复制到本地文件。
响应
路由处理程序支持以下类型的返回值:
- 基于
IResult
- 这包括Task<IResult>
和ValueTask<IResult>
ValueTask<string>
- 这包括string
和Task<string>
T
(任何其他类型)- 这包括Task<T>
和ValueTask<T>
返回值 | 行为 | Content-Type |
---|---|---|
IResult |
框架调用 IResult.ExecuteAsync | 由 IResult 实现决定 |
string |
框架将字符串直接写入响应 | text/plain |
T (任何其他类型) |
框架将 JSON 序列化响应 | application/json |
示例返回值
字符串返回值
app.MapGet("/hello", () => "Hello World");
JSON 返回值
app.MapGet("/hello", () => new { Message = "Hello World" });
IResult 返回值
app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));
以下示例使用内置结果类型自定义响应:
app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound())
.Produces<Todo>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
自定义状态代码
app.MapGet("/405", () => Results.StatusCode(405));
文本
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () =>
{
var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
// Proxy the response as JSON
return Results.Stream(stream, "application/json");
});
app.Run();
重定向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
文件
app.MapGet("/download", () => Results.File("myfile.text"));
内置结果
Microsoft.AspNetCore.Http.Results
静态类中存在常见的结果帮助程序。
说明 | 响应类型 | 状态代码 | API |
---|---|---|---|
使用高级选项编写 JSON 响应 | application/json | 200 | Results.Json |
编写 JSON 响应 | application/json | 200 | Results.Ok |
编写文本响应 | text/plain(默认),可配置 | 200 | Results.Text |
将响应编写为字节 | application/octet-stream(默认),可配置 | 200 | Results.Bytes |
将字节流写入响应 | application/octet-stream(默认),可配置 | 200 | Results.Stream |
使用 content-disposition 标头将文件流式传输至响应以下载 | application/octet-stream(默认),可配置 | 200 | Results.File |
将状态代码设置为 404,并包含可选的 JSON 响应 | 空值 | 404 | Results.NotFound |
将状态代码设置为 204 | 空值 | 204 | Results.NoContent |
将状态代码设置为 422,并包含可选的 JSON 响应 | 空值 | 422 | Results.UnprocessableEntity |
将状态代码设置为 400,并包含可选的 JSON 响应 | 空值 | 400 | Results.BadRequest |
将状态代码设置为 409,并包含可选的 JSON 响应 | 空值 | 409 | Results.Conflict |
将问题详细信息 JSON 对象写入响应 | 空值 | 500(默认),可配置 | Results.Problem |
将问题详细信息 JSON 对象写入包含验证错误的响应 | 空值 | 空值,可配置 | Results.ValidationProblem |
自定义结果
应用程序可以通过实现自定义 IResult 类型来控制响应。 以下代码是 HTML 结果类型的示例:
using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
public static IResult Html(this IResultExtensions resultExtensions, string html)
{
ArgumentNullException.ThrowIfNull(resultExtensions);
return new HtmlResult(html);
}
}
class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
建议将扩展方法添加到 Microsoft.AspNetCore.Http.IResultExtensions,使这些自定义结果更容易发现。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
<head><title>miniHTML</title></head>
<body>
<h1>Hello World</h1>
<p>The time on the server is {DateTime.Now:O}</p>
</body>
</html>"));
app.Run();
授权
可以使用授权策略保护路由。 可以通过 [Authorize]
属性或通过使用 RequireAuthorization 方法声明这些对象:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
前面的代码可以用 RequireAuthorization 编写:
app.MapGet("/auth", () => "This endpoint requires authorization")
.RequireAuthorization();
以下示例使用基于策略的授权:
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly",
b => b.RequireClaim("admin", "true")));
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.UseAuthorization();
app.MapGet("/admin", [Authorize("AdminsOnly")] () =>
"The /admin endpoint is for admins only.");
app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
.RequireAuthorization("AdminsOnly");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");
app.Run();
允许未经身份验证的用户访问终结点
[AllowAnonymous]
允许未经身份验证的用户访问终结点:
app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");
app.MapGet("/login2", () => "This endpoint also for all roles.")
.AllowAnonymous();
CORS
路由可以使用 CORS 策略启用 CORS。 可以通过 [EnableCors]
属性或通过使用 RequireCors 方法声明 CORS。 以下示例启用 CORS:
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/",() => "Hello CORS!");
app.Run();
using Microsoft.AspNetCore.Cors;
const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder =>
{
builder.WithOrigins("http://example.com",
"http://www.contoso.com");
});
});
var app = builder.Build();
app.UseCors();
app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () =>
"This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
.RequireCors(MyAllowSpecificOrigins);
app.Run();
有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS)