最小 API 快速参考

您所在的位置:网站首页 深入浅出aspnetcore 最小 API 快速参考

最小 API 快速参考

2023-09-02 21:41| 来源: 网络整理| 查看: 265

最小 API 快速参考 项目 06/23/2023

此文档:

提供有关最小 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 类的新实例。

WebApplication 根据某些条件自动添加以下中间件:

当 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(); var app = builder.Build(); app.MapControllers(); using (var scope = app.Services.CreateScope()) { var sampleService = scope.ServiceProvider.GetRequiredService(); 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 中的配置中的默认配置

var builder = WebApplication.CreateBuilder(args); var message = builder.Configuration["HelloKey"] ?? "Hello"; var app = builder.Build(); app.MapGet("/", () => message); app.Run(); 读取环境

以下代码从配置中读取 HelloKey,并在 / 终结点显示值。 如果配置值为 null,则“Hello”将分配给 message:

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(); 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(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(); 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 根据某些条件自动添加以下中间件:

当 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(); var app = builder.Build(); app.MapControllers(); using (var scope = app.Services.CreateScope()) { var sampleService = scope.ServiceProvider.GetRequiredService(); 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(); 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(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 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 中的路由中的路由约束参考。

路由组

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(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 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(); 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(); // ... }); 显式参数绑定

特性可用于显式声明绑定参数的位置。

using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Added as service builder.Services.AddSingleton(); 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 存储库中的完整示例代码。

通过依赖关系注入进行参数绑定

当类型配置为服务时,最小 API 的参数绑定通过依赖关系注入绑定参数。 无需将 [FromServices] 属性显式应用于参数。 在以下代码中,这两个操作返回时间:

using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton(); 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 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 _queue; private readonly ILogger _logger; public BackgroundQueue(Channel queue, ILogger 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(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 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.CreateBounded(maxQueueSize)); // Create a background queue service. builder.Services.AddHostedService(); var app = builder.Build(); // curl --request POST 'https://localhost:/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }' // curl --request POST "https://localhost:/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }" app.MapPost("/register", async (HttpRequest req, Stream body, Channel 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 标头支持经过身份验证的文件上传请求。

没有针对防伪造的内置支持。 但是,这可以使用 IAntiforgery 服务来实现。

使用 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(); 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 = $""" """; return Results.Content(html, "text/html"); }); app.MapPost("/upload", async Task (IFormFile file, HttpContext context, IAntiforgery antiforgery) => { try { await antiforgery.ValidateRequestAsync(context); var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName); await UploadFileWithName(file, fileSaveName); return TypedResults.Ok("File uploaded successfully!"); } catch (AntiforgeryValidationException e) { return TypedResults.BadRequest("Invalid anti-forgery token"); } }); 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(); 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 = $""" """; return Results.Content(html, "text/html"); }); app.MapPost("/upload", async Task (IFormFile file, HttpContext context, IAntiforgery antiforgery) => { try { await antiforgery.ValidateRequestAsync(context); var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName); await UploadFileWithName(file, fileSaveName); return TypedResults.Ok("File uploaded successfully!"); } catch (AntiforgeryValidationException e) { return TypedResults.BadRequest("Invalid anti-forgery token"); } }); app.Run();

有关 XSRF 攻击的详细信息,请参阅跨网站请求伪造 (XSRF/CSRF) 攻击。

绑定到窗体中的连接和复杂类型

以下各项支持绑定:

集合,例如列表和字典 复杂类型,例如 Todo 或 Project

下面的代码演示:

将多部分表单输入绑定到复杂对象的最小终结点。 如何使用防伪服务来支持生成和验证防伪令牌。 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.MapGet("/", (HttpContext context, IAntiforgery antiforgery) => { var token = antiforgery.GetAndStoreTokens(context); var html = $""" """; return Results.Content(html, "text/html"); }); app.MapPost("/todo", async Task ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) => { try { await antiforgery.ValidateRequestAsync(context); return TypedResults.Ok(todo); } catch (AntiforgeryValidationException e) { return TypedResults.BadRequest("Invalid anti-forgery 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 正文中读取的参数进行区分。 目前不支持绑定到记录类型,因此必须以类的形式实现复杂类型。 不支持递归绑定,因此类型 Todo 不包含任何递归引用。

绑定标头和查询字符串中的数组和字符串值

下面的代码展示如何将查询字符串绑定到基元类型、字符串数组和 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); });

使用 Postman 之类的工具将以下数据传递给上一终结点:

[ { "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(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 BindAsync(HttpContext context, ParameterInfo parameter); public static ValueTask 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 BindAsync(HttpContext context, ParameterInfo parameter) { const string sortByKey = "sortBy"; const string sortDirectionKey = "sortDir"; const string currentPageKey = "page"; Enum.TryParse(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(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] 窗体:[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(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 访问请求正文。 将请求正文复制到本地文件。

参数绑定是将请求数据转换为由路由处理程序表示的强类型参数的过程。 绑定源确定绑定参数的位置。 绑定源可以是显式的,也可以是基于 HTTP 方法和参数类型推断的。

支持的绑定源:

路由值 查询字符串 标头 主体(如 JSON) 依赖项注入提供的服务 自定义

.NET 6 和 7 本身不支持从窗体值进行绑定。

下面的 GET 路由处理程序使用其中一些参数绑定源:

var builder = WebApplication.CreateBuilder(args); // Added as service builder.Services.AddSingleton(); 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(); // ... }); 显式参数绑定

特性可用于显式声明绑定参数的位置。

using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Added as service builder.Services.AddSingleton(); 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(); 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 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 _queue; private readonly ILogger _logger; public BackgroundQueue(Channel queue, ILogger 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(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 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.CreateBounded(maxQueueSize)); // Create a background queue service. builder.Services.AddHostedService(); var app = builder.Build(); // curl --request POST 'https://localhost:/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }' // curl --request POST "https://localhost:/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }" app.MapPost("/register", async (HttpRequest req, Stream body, Channel 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 标头支持经过身份验证的文件上传请求。

没有针对防伪造的内置支持。 但是,这可以使用 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); });

使用 Postman 之类的工具将以下数据传递给上一终结点:

[ { "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(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 BindAsync(HttpContext context, ParameterInfo parameter); public static ValueTask 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 BindAsync(HttpContext context, ParameterInfo parameter) { const string sortByKey = "sortBy"; const string sortDirectionKey = "sortDir"; const string currentPageKey = "page"; Enum.TryParse(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(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) => {});),则会将其从路由中绑定。 从查询字符串进行绑定。 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。 参数来自正文。 为正文绑定配置 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(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 和 ValueTask ValueTask - 这包括 string 和 Task T(任何其他类型)- 这包括 Task 和 ValueTask 返回值 行为 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(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://consoto/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(@$" miniHTML Hello World

The time on the server is {DateTime.Now:O}

")); 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)); } }

可以查看静态 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(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); 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(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); 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 应用中的 OpenAPI 在最小 API 应用程序中创建响应 最小 API 筛选器应用 处理最小 API 应用中的错误 最小 API 中的身份验证和授权 测试最小 API 应用

此文档:

提供有关最小 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(); var app = builder.Build(); app.MapControllers(); using (var scope = app.Services.CreateScope()) { var sampleService = scope.ServiceProvider.GetRequiredService(); 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(); 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(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(); 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(); // ... }); 显式参数绑定

特性可用于显式声明绑定参数的位置。

using Microsoft.AspNetCore.Mvc; var builder = WebApplication.CreateBuilder(args); // Added as service builder.Services.AddSingleton(); 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(); 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 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 BindAsync(HttpContext context, ParameterInfo parameter); public static ValueTask 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 BindAsync(HttpContext context, ParameterInfo parameter) { const string sortByKey = "sortBy"; const string sortDirectionKey = "sortDir"; const string currentPageKey = "page"; Enum.TryParse(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(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] 特殊类型 HttpContext HttpRequest (HttpContext.Request) HttpResponse (HttpContext.Response) ClaimsPrincipal (HttpContext.User) CancellationToken (HttpContext.RequestAborted) 参数类型具有有效的 BindAsync 方法。 参数类型为字符串或具有有效的 TryParse 方法。 如果路由模板中存在参数名称(例如 app.Map("/todo/{id}", (int id) => {});),则会将其从路由中绑定。 从查询字符串进行绑定。 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。 参数来自正文。 自定义 JSON 绑定

正文绑定源使用 System.Text.Json 进行反序列化。 不能更改此默认值,但可以使用前面所述的其他方法自定义绑定。 若要自定义 JSON 序列化程序选项,请使用类似于下面的代码:

using Microsoft.AspNetCore.Http.Json; var builder = WebApplication.CreateBuilder(args); // Configure JSON options. builder.Services.Configure(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 和 ValueTask ValueTask - 这包括 string 和 Task T(任何其他类型)- 这包括 Task 和 ValueTask 返回值 行为 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(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://consoto/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(@$" miniHTML Hello World

The time on the server is {DateTime.Now:O}

")); 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(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); 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(options => options.UseSqlServer(connectionString)); builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores(); 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 中的 OpenAPI 支持



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3