最小 API 快速参考

此文档:

最小的 API 包括:

WebApplication

以下代码由 ASP.NET Core 模板生成:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

可以通过命令行上的 dotnet new web 或在 Visual Studio 中选择“空 Web”模板来创建前面的代码。

以下代码创建 WebApplication (app),而无需显式创建 WebApplicationBuilder

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create 使用预配置默认值初始化 WebApplication 类的新实例。

根据某些条件,WebApplication 会自动在 Minimal API applications 中添加以下中间件:

以下代码实际上是添加到应用程序的自动中间件生成的代码:

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 => {});

在某些情况下,应用程序的默认中间件配置不正确,需要修改。 例如,应在 UseAuthenticationUseAuthorization 前调用 UseCors。 如果调用 UseCors,应用需要调用 UseAuthenticationUseAuthorization

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

如果在路由匹配发生前应运行中间件,则应调用 UseRouting,并且应在调用 UseRouting 之前放置中间件。 在这种情况下,不需要 UseEndpoints,因为它会自动添加,如上所述:

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

添加终端中间件时:

  • 必须在 UseEndpoints 后添加中间件。
  • 应用需要调用 UseRoutingUseEndpoints,以便终端中间件可以放置在正确的位置。
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

多个端口

在以下代码中,应用响应端口 30004000

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_PORTSASPNETCORE_HTTP_PORTS

ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000

有关详细信息,请参阅为 ASP.NET Core Kestrel Web 服务器配置终结点

指定使用开发证书的 HTTPS

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

有关开发证书详细信息,请参阅在 Windows 和 macOS 上信任 ASP.NET Core HTTPS 开发证书

指定使用自定义证书的 HTTPS

以下部分显示如何使用 appsettings.json 文件和通过配置指定自定义证书。

使用 appsettings.json 指定自定义证书

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

通过配置指定自定义证书

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

使用证书 API

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

读取环境

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

有关使用环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境

配置

以下代码从配置系统读取:

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

有关详细信息,请参阅 ASP.NET Core 中的配置

Logging

以下代码在应用程序启动时将消息写入日志:

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录

访问依赖项注入 (DI) 容器

下面的代码演示如何在应用程序启动过程中从 DI 容器获取服务:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

以下代码演示如何使用 [FromKeyedServices] 属性从 DI 容器访问密钥:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

有关 DI 的详细信息,请参阅 ASP.NET Core 中的依赖项注入

WebApplicationBuilder

本部分包含使用 WebApplicationBuilder 的示例代码。

更改内容根、应用程序名称和环境

以下代码设置内容根、应用程序名称和环境:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder 使用预配置的默认值初始化 WebApplicationBuilder 类的新实例。

有关详细信息,请参阅 ASP.NET Core 基础知识概述

使用环境变量或命令行更改内容根、应用程序名称和环境

下表显示了用于更改内容根、应用程序名称和环境的环境变量及命令行参数:

feature 环境变量 命令行参数
应用程序名称 ASPNETCORE_APPLICATIONNAME --applicationName
环境名称 ASPNETCORE_ENVIRONMENT --environment
内容根 ASPNETCORE_CONTENTROOT --contentRoot

添加配置提供程序

以下示例添加 INI 配置提供程序:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

有关详细信息,请参阅 ASP.NET Core 中的配置中的文件配置提供程序

读取配置

默认情况下,WebApplicationBuilder 从多个源读取配置,包括:

  • appSettings.jsonappSettings.{environment}.json
  • 环境变量
  • 命令行

有关读取的配置源的完整列表,请参阅 ASP.NET Core 中的配置中的默认配置

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

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

读取环境

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

添加日志记录提供程序

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

添加服务

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

自定义 IHostBuilder

可以使用 IHostBuilder访问 IHostBuilder 上的现有扩展方法:

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

自定义 IWebHostBuilder

可以使用 IWebHostBuilder 属性访问 IWebHostBuilder 上的扩展方法。

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

更改 Web 根

默认情况下,Web 根相对于 wwwroot 文件夹中的内容根。 Web 根是静态文件中间件查找静态文件的位置。 可以使用 WebHostOptions、命令行或 UseWebRoot 方法更改 Web 根:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

自定义依赖项注入 (DI) 容器

下面的示例使用 Autofac

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

添加中间件

可以在 WebApplication 上配置任何现有的 ASP.NET Core 中间件:

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

有关详细信息,请参阅 ASP.NET Core 中间件

开发人员异常页

WebApplication.CreateBuilder 使用预配置默认值初始化 WebApplicationBuilder 类的新实例。 开发人员异常页在预配置的默认值中启用。 当在开发环境中运行以下代码时,导航到 / 以呈现一个显示异常的友好页面。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core 中间件

下表列出了一些经常与最小 API 一起使用的中间件。

中间件 描述 API
身份验证 提供身份验证支持。 UseAuthentication
授权 提供身份验证支持。 UseAuthorization
CORS 配置跨域资源共享。 UseCors
异常处理程序 全局处理中间件管道引发的异常。 UseExceptionHandler
转接头 将代理标头转发到当前请求。 UseForwardedHeaders
HTTPS 重定向 将所有 HTTP 请求重定向到 HTTPS。 UseHttpsRedirection
HTTP 严格传输安全性 (HSTS) 添加特殊响应标头的安全增强中间件。 UseHsts
请求日志记录 提供对记录 HTTP 请求和响应的支持。 UseHttpLogging
请求超时 支持配置请求超时、全局默认值和每个终结点。 UseRequestTimeouts
W3C 请求日志记录 提供对以 W3C 格式记录 HTTP 请求和响应的支持。 UseW3CLogging
响应缓存 提供对缓存响应的支持。 UseResponseCaching
响应压缩 提供对压缩响应的支持。 UseResponseCompression
会话 提供对管理用户会话的支持。 UseSession
静态文件 为提供静态文件和目录浏览提供支持。 UseStaticFilesUseFileServer
WebSockets 启用 WebSockets 协议。 UseWebSockets

以下各部分介绍请求处理:路由、参数绑定和响应。

路由

配置的 WebApplication 支持 Map{Verb}MapMethods,其中 {Verb} 是一种采用大小写混写格式的 HTTP 方法,如 GetPostPutDelete

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

路由处理程序可以声明要捕获的参数。 当向带有声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,userIdbookId 均为 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 扩展方法有助于组织具有共同前缀的终结点组。 它减少了重复代码,并允许通过对添加终结点元数据RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

例如,以下代码创建两组相似的终结点:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

在此方案中,可以适用 201 Created 结果中 Location 标头的相对地址:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

第一组终结点将仅匹配前缀为 /public/todos 并且无需任何身份验证即可访问的请求。 第二组终结点将仅匹配前缀为 /private/todos 并且需要身份验证的请求。

QueryPrivateTodos终结点筛选器工厂是一种本地函数,用于修改路由处理程序的 TodoDb 参数,以允许访问和存储专用的 todo 数据。

路由组还支持嵌套组和具有路由参数和约束的复杂前缀模式。 在以下示例中,映射到 user 组的路由处理程序可以捕获外部组前缀中定义的 {org}{group} 路由参数。

前缀也可以为空。 这对于在不更改路由模式的情况下向一组终结点添加终结点元数据或筛选器非常有用。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

向组添加筛选器或元数据的行为与在添加可能已添加到内部组或特定终结点的任何额外筛选器或元数据之前将它们单独添加到每个终结点的行为相同。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

在上面的示例中,外部筛选器将在内部筛选器之前记录传入请求,即使它是第二个添加的。 由于筛选器应用于不同的组,因此它们相对于彼此的添加顺序并不重要。 如果应用于相同的组或特定终结点,则筛选器的添加顺序非常重要。

/outer/inner/ 的请求将记录以下内容:

/outer group filter
/inner group filter
MapGet filter

参数绑定

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

支持的绑定源:

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

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

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

下表显示了前面的示例中使用的参数与关联的绑定源之间的关系。

参数 绑定源
id 路由值
page 查询字符串
customHeader 标头的值开始缓存响应
service 由依赖项注入提供

HTTP 方法 GETHEADOPTIONSDELETE 不会从正文隐式绑定。 如需从这些 HTTP 方法的主体(如 JSON)进行绑定,可以使用 [FromBody]显式绑定或从 HttpRequest 读取。

以下示例 POST 路由处理程序将主体(如 JSON)的绑定源用于 person 参数:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

上述示例中的参数都自动从请求数据绑定。 为了演示参数绑定的便利性,以下路由处理程序演示如何直接从请求读取请求数据:

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

显式参数绑定

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

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
参数 绑定源
id 名称为 id 的路由值
page 名称为 "p" 的查询字符串
service 由依赖项注入提供
contentType 名称为 "Content-Type" 的标头

从窗体值显式绑定

[FromForm] 属性绑定窗体值:

app.MapPost("/todos", async ([FromForm] string name,
    [FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = name,
        Visibility = visibility
    };

    if (attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await attachment.CopyToAsync(stream);
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.

另一种方法是将 [AsParameters] 属性与带有使用 [FromForm] 进行注释的属性的自定义类型一起使用。 例如,以下代码从窗体值绑定到 NewTodoRequest 记录结构的属性:

app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = request.Name,
        Visibility = request.Visibility
    };

    if (request.Attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await request.Attachment.CopyToAsync(stream);

        todo.Attachment = attachmentName;
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
    [FromForm] Visibility Visibility, IFormFile? Attachment);

有关详细信息,请参阅本文后面的 AsParameters 部分。

AspNetCore.Docs.Samples 存储库中的完整示例代码

IFormFile 和 IFormFileCollection 的安全绑定

可以通过 IFormFileIFormFileCollection[FromForm] 来支持复杂表单绑定:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
    ([FromForm] FileUploadForm fileUploadForm, HttpContext context,
                                                IAntiforgery antiforgery) =>
{
    await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
              fileUploadForm.Name!, app.Environment.ContentRootPath);
    return TypedResults.Ok($"Your file with the description:" +
        $" {fileUploadForm.Description} has been uploaded successfully");
});

app.Run();

通过 [FromForm] 绑定到请求的参数包括防伪令牌。 处理请求时会验证防伪令牌。 有关详细信息,请参阅用最小 API 实现防伪

有关详细信息,请参阅最小 API 中的表单绑定

AspNetCore.Docs.Samples 存储库中的完整示例代码

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

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

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

可选参数

在路由处理程序中声明的参数被视为必需参数:

  • 如果请求与路由匹配,则只有在请求中提供了所有必需的参数时,路由处理程序才会运行。
  • 未能提供所有必需的参数会导致错误。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products BadHttpRequestException:查询字符串中未提供必需的参数“int pageNumber”。
/products/1 HTTP 404 错误,无匹配的路由

若要设置为 pageNumber 可选,请将类型定义为可选,或提供默认值:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products 已返回 1
/products2 已返回 1

前面的可为空默认值适用于所有源:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();

如果未发送请求正文,则前面的代码将使用 null 产品调用方法。

注意:如果提供的数据无效并且参数可为空,则路由处理程序不运行。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products 已返回 1
/products?pageNumber=two BadHttpRequestException:无法从“two”绑定参数 "Nullable<int> pageNumber"
/products/two HTTP 404 错误,无匹配的路由

有关详细信息,请参阅绑定失败部分。

特殊类型

以下类型在绑定时没有显式特性:

  • HttpContext:包含有关当前 HTTP 请求或响应的所有信息的上下文:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: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);
    

将请求正文绑定为 StreamPipeReader

请求正文可以绑定为 StreamPipeReader,以有效支持用户必须处理数据的情况,以及:

  • 将数据存储在 Blob 存储中,或将数据排入队列提供程序的队列。
  • 使用工作进程或云功能处理存储的数据。

例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。

以下代码可实现后台队列:

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

以下代码将请求正文绑定到 Stream

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

以下代码显示完整的 Program.cs 文件:

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • 读取数据时,Stream 是与 HttpRequest.Body 相同的对象。
  • 默认情况下,不缓冲请求正文。 读取正文后,不支持后退。 无法多次读取流。
  • 不能在最小操作处理程序之外使用 StreamPipeReader,因为基础缓冲区将被释放或重用。

使用 IFormFile 和 IFormFileCollection 上传文件

以下代码使用 IFormFileIFormFileCollection 上传文件:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

使用授权标头客户端证书或 cookie 标头支持经过身份验证的文件上传请求。

使用 IFormCollection、IFormFile 和 IFormFileCollection 绑定到表单

支持使用 IFormCollectionIFormFileIFormFileCollection 从基于表单的参数进行绑定。 OpenAPI 元数据针对表单参数进行推断,以支持与 Swagger UI 集成。

以下代码使用从 IFormFile 类型推断的绑定上传文件:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

警告:实现表单时,应用必须防止跨站点请求伪造 (XSRF/CSRF) 攻击。 在上述代码中,IAntiforgery 服务用于通过生成和验证防伪令牌来防止 XSRF 攻击:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

有关 XSRF 攻击的详细信息,请参阅使用最少 API 进行防伪造

有关详细信息,请参阅 Form binding in minimal APIs(最小 API 中的表单绑定);

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

以下支持绑定:

  • 集合,例如列表字典
  • 复杂类型,例如 TodoProject

下面的代码演示:

  • 将多部分表单输入绑定到复杂对象的最小终结点。
  • 如何使用防伪服务来支持生成和验证防伪令牌。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
        <html><body>
           <form action="/todo" method="POST" enctype="multipart/form-data">
               <input name="{token.FormFieldName}" 
                                type="hidden" value="{token.RequestToken}" />
               <input type="text" name="name" />
               <input type="date" name="dueDate" />
               <input type="checkbox" name="isCompleted" value="true" />
               <input type="submit" />
               <input name="isCompleted" type="hidden" value="false" /> 
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>> 
               ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid 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 正文中读取的参数进行区分。
  • 使用请求委托生成器编译的最小 API 不支持从复杂或集合类型进行绑定
  • 该标记显示一个额外的隐藏输入,其名称为 isCompleted,值为 false。 如果在提交表单时选中了 isCompleted 复选框,则 truefalse 两个值都作为值提交。 如果未选中该复选框,则仅提交隐藏输入值 false。 ASP.NET Core 模型绑定进程在绑定到 bool 值时仅读取第一个值,这导致选中的复选框为 true,未选中的复选框为 false

提交到上述终结点的表单数据示例如下所示:

__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false

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

下面的代码展示如何将查询字符串绑定到基元类型、字符串数组和 StringValues 数组:

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

在类型实现 TryParse 时,支持将查询字符串或标头值绑定到复杂类型的数组。 以下代码绑定到字符串数组,并返回具有指定标记的所有项:

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

以下代码显示了模型和所需的 TryParse 实现:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

以下代码绑定到 int 数组:

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

若要测试上述代码,请添加以下终结点以使用 Todo 项填充数据库:

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

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

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

以下代码绑定到标头键 X-Todo-Id,并返回具有匹配 Id 值的 Todo 项:

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

注意

从查询字符串绑定 string[] 时,缺少任何匹配的查询字符串值都会导致空数组而不是 null 值。

使用 [AsParameters] 对参数列表进行参数绑定

AsParametersAttribute 启用对类型的简单参数绑定而不是复杂或递归模型绑定。

考虑下列代码:

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

请考虑以下 GET 终结点:

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

以下 struct 可用于替换上述突出显示的参数:

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

重构的 GET 终结点将上述 structAsParameters 属性一起使用:

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

structAsParameters 一起使用可能比使用 record 类型性能更佳。

AspNetCore.Docs.Samples 存储库中的完整示例代码

自定义绑定

自定义参数绑定有两种方法:

  1. 对于路由、查询和标头绑定源,通过添加类型的静态 TryParse 方法来绑定自定义类型。
  2. 通过对类型实现 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.1Point: 12.3, 10.1

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync 具有以下 API:

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

下面的代码显示带有 URI /products?SortBy=xyz&SortDir=Desc&Page=99SortBy:xyz, SortDirection:Desc, CurrentPage:99

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

绑定失败

绑定失败时,框架会记录调试消息,并根据失败模式将各种状态代码返回到客户端。

故障模式 可为空参数类型 绑定源 状态代码
{ParameterType}.TryParse 返回 false route/query/header 400
{ParameterType}.BindAsync 返回 null 自定义 400
{ParameterType}.BindAsync 引发 不重要 custom 500
未能反序列化 JSON 正文 不重要 body 400
错误的内容类型(不是 application/json 不重要 body 415

绑定优先级

用于从参数确定绑定源的规则:

  1. 按以下顺序在参数(From* 属性)上定义的显式属性:
    1. 路由值:[FromRoute]
    2. 查询字符串:[FromQuery]
    3. 标头:[FromHeader]
    4. 正文:[FromBody]
    5. 窗体:[FromForm]
    6. 一个服务:[FromServices]
    7. 参数值:[AsParameters]
  2. 特殊类型
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormCollection (HttpContext.Request.Form)
    7. IFormFileCollection (HttpContext.Request.Form.Files)
    8. IFormFile (HttpContext.Request.Form.Files[paramName])
    9. Stream (HttpContext.Request.Body)
    10. PipeReader (HttpContext.Request.BodyReader)
  3. 参数类型具有有效的静态 BindAsync 方法。
  4. 参数类型为字符串或具有有效的静态 TryParse 方法。
    1. 如果路由模板中存在参数名称(例如 app.Map("/todo/{id}", (int id) => {});),则将从路由中绑定它。
    2. 从查询字符串进行绑定。
  5. 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
  6. 参数来自正文。

为正文绑定配置 JSON 反序列化选项

正文绑定源使用 System.Text.Json 进行反序列化。 不能更改此默认值,但可以配置 JSON 序列化和反序列化选项

全局配置 JSON 反序列化选项

全局应用于应用的选项可以通过调用 ConfigureHttpJsonOptions 进行配置。 以下示例包括公共字段,并设置 JSON 输出的格式。

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

由于示例代码同时配置序列化和反序列化,因此它可以读取 NameField 并在输出 JSON 中包含 NameField

为终结点配置 JSON 反序列化选项

ReadFromJsonAsync 具有接受 JsonSerializerOptions 对象的重载。 以下示例包括公共字段,并设置 JSON 输出的格式。

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

由于上述代码仅将自定义选项应用于反序列化,输出 JSON 不包括 NameField

读取请求正文

使用 HttpContextHttpRequest 参数直接读取请求正文:

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();

前面的代码:

响应

路由处理程序支持以下类型的返回值:

  1. 基于 IResult - 这包括 Task<IResult>ValueTask<IResult>
  2. ValueTask<string> - 这包括 stringTask<string>
  3. T(任何其他类型)- 这包括 Task<T>ValueTask<T>
返回值 行为 Content-Type
IResult 框架调用 IResult.ExecuteAsync IResult 实现决定
string 框架将字符串直接写入响应 text/plain
T(任何其他类型) 框架 JSON 序列化响应 application/json

有关路由处理程序返回值的更深入指南,请参阅在最小 API 应用程序中创建响应

示例返回值

字符串返回值

app.MapGet("/hello", () => "Hello World");

JSON 返回值

app.MapGet("/hello", () => new { Message = "Hello World" });

返回 TypedResults

以下代码将返回 TypedResults

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

优先返回 TypedResults,而不是 Results。 有关详细信息,请参阅 TypedResults 与 Results

IResult 返回值

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

以下示例使用内置结果类型自定义响应:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

自定义状态代码

app.MapGet("/405", () => Results.StatusCode(405));

文本

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

有关更多示例,请参阅在最小 API 应用程序中创建响应

重定向

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

文件

app.MapGet("/download", () => Results.File("myfile.text"));

内置结果

ResultsTypedResults 静态类中存在常见的结果帮助程序。 优先返回 TypedResults,而不是 Results。 有关详细信息,请参阅 TypedResults 与 Results

自定义结果

应用程序可以通过实现自定义 IResult 类型来控制响应。 以下代码是 HTML 结果类型的示例:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

建议将扩展方法添加到 Microsoft.AspNetCore.Http.IResultExtensions,使这些自定义结果更容易发现。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

类型化结果

IResult 接口可以表示从最小 API 返回的值,这些 API 不利用对 JSON 将返回的对象序列化为 HTTP 响应的隐式支持。 静态 Results 类用于创建各种 IResult 对象,这些对象表示不同类型的响应。 例如,设置响应状态代码或重定向到另一个 URL。

实现 IResult 的类型是公共类型,允许在测试时使用类型断言。 例如:

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

可以查看静态 TypedResults 类上相应方法的返回类型,以找到要转换为的正确公共 IResult 类型。

有关更多示例,请参阅在最小 API 应用程序中创建响应

筛选器

请参阅:

授权

可以使用授权策略保护路由。 可以通过 [Authorize] 属性或通过使用 RequireAuthorization 方法声明这些对象:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

前面的代码可以用 RequireAuthorization 编写:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

以下示例使用基于策略的授权

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

允许未经身份验证的用户访问终结点

[AllowAnonymous] 允许未经身份验证的用户访问终结点:

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

路由可以使用 CORS 策略启用 CORS。 可以通过 [EnableCors] 属性或通过使用 RequireCors 方法声明 CORS。 以下示例启用 CORS:

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS)

ValidateScopes 和 ValidateOnBuild

ValidateScopesValidateOnBuild部署环境中默认启用,但在其他环境中禁用。

ValidateOnBuildtrue 时,DI 容器会在生成时验证服务配置。 如果服务配置无效,则生成在应用启动时失败,而不是在运行时请求服务时失败。

ValidateScopestrue 时,DI 容器会验证是否未从根范围解析范围服务。 从根范围解析范围服务可能会导致内存泄漏,因为服务在内存中保留的时间比请求的范围要长。

出于性能原因,ValidateScopesValidateOnBuild 在非开发模式下默认为 false。

以下代码显示,ValidateScopes 在开发模式下默认启用,但在发布模式下禁用:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }

以下代码显示,ValidateOnBuild 在开发模式下默认启用,但在发布模式下禁用:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }

以下代码会在 Development 中禁用 ValidateScopesValidateOnBuild

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}

另请参阅

此文档:

最小的 API 包括:

WebApplication

以下代码由 ASP.NET Core 模板生成:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

可以通过命令行上的 dotnet new web 或在 Visual Studio 中选择“空 Web”模板来创建前面的代码。

以下代码创建 WebApplication (app),而无需显式创建 WebApplicationBuilder

var app = WebApplication.Create(args);

app.MapGet("/", () => "Hello World!");

app.Run();

WebApplication.Create 使用预配置默认值初始化 WebApplication 类的新实例。

根据某些条件,WebApplication 会自动在 Minimal API applications 中添加以下中间件:

以下代码实际上是添加到应用程序的自动中间件生成的代码:

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 => {});

在某些情况下,应用程序的默认中间件配置不正确,需要修改。 例如,应在 UseAuthenticationUseAuthorization 前调用 UseCors。 如果调用 UseCors,应用需要调用 UseAuthenticationUseAuthorization

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

如果在路由匹配发生前应运行中间件,则应调用 UseRouting,并且应在调用 UseRouting 之前放置中间件。 在这种情况下,不需要 UseEndpoints,因为它会自动添加,如上所述:

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

添加终端中间件时:

  • 必须在 UseEndpoints 后添加中间件。
  • 应用需要调用 UseRoutingUseEndpoints,以便终端中间件可以放置在正确的位置。
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

多个端口

在以下代码中,应用响应端口 30004000

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

从命令行设置端口

以下命令使应用响应端口 7777

dotnet run --urls="https://localhost:7777"

如果在 appsettings.json 文件中也配置了 Kestrel 终结点,则使用 appsettings.json 文件指定的 URL。 有关详细信息,请参阅Kestrel终结点配置

从环境中读取端口

以下代码从环境中读取端口:

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

从环境设置端口的首选方法为使用 ASPNETCORE_URLS 环境变量,如以下部分所示。

通过 ASPNETCORE_URLS 环境变量设置端口

ASPNETCORE_URLS 环境变量可用于设置端口:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS 支持多个 URL:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

有关使用环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境

侦听所有接口

以下示例演示如何侦听所有接口

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

使用 ASPNETCORE_URLS 侦听所有接口

前面的示例可以使用 ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

指定使用开发证书的 HTTPS

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

有关开发证书详细信息,请参阅在 Windows 和 macOS 上信任 ASP.NET Core HTTPS 开发证书

指定使用自定义证书的 HTTPS

以下部分显示如何使用 appsettings.json 文件和通过配置指定自定义证书。

使用 appsettings.json 指定自定义证书

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

通过配置指定自定义证书

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

使用证书 API

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

配置

以下代码从配置系统读取:

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

有关详细信息,请参阅 ASP.NET Core 中的配置

Logging

以下代码在应用程序启动时将消息写入日志:

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录

访问依赖项注入 (DI) 容器

下面的代码演示如何在应用程序启动过程中从 DI 容器获取服务:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

有关详细信息,请参阅 ASP.NET Core 中的依赖项注入

WebApplicationBuilder

本部分包含使用 WebApplicationBuilder 的示例代码。

更改内容根、应用程序名称和环境

以下代码设置内容根、应用程序名称和环境:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder 使用预配置的默认值初始化 WebApplicationBuilder 类的新实例。

有关详细信息,请参阅 ASP.NET Core 基础知识概述

按环境变量或命令行更改内容根、应用程序名称和环境

下表显示了用于更改内容根、应用程序名称和环境的环境变量及命令行参数:

feature 环境变量 命令行参数
应用程序名称 ASPNETCORE_APPLICATIONNAME --applicationName
环境名称 ASPNETCORE_ENVIRONMENT --environment
内容根 ASPNETCORE_CONTENTROOT --contentRoot

添加配置提供程序

以下示例添加 INI 配置提供程序:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

有关详细信息,请参阅 ASP.NET Core 中的配置中的文件配置提供程序

读取配置

默认情况下,WebApplicationBuilder 从多个源读取配置,包括:

  • appSettings.jsonappSettings.{environment}.json
  • 环境变量
  • 命令行

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

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

有关读取的配置源的完整列表,请参阅 ASP.NET Core 中的配置中的默认配置

添加日志记录提供程序

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

添加服务

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

自定义 IHostBuilder

可以使用 IHostBuilder访问 IHostBuilder 上的现有扩展方法:

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

自定义 IWebHostBuilder

可以使用 IWebHostBuilder 属性访问 IWebHostBuilder 上的扩展方法。

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

更改 Web 根

默认情况下,Web 根相对于 wwwroot 文件夹中的内容根。 Web 根是静态文件中间件查找静态文件的位置。 可以使用 WebHostOptions、命令行或 UseWebRoot 方法更改 Web 根:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

自定义依赖项注入 (DI) 容器

下面的示例使用 Autofac

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

添加中间件

可以在 WebApplication 上配置任何现有的 ASP.NET Core 中间件:

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

有关详细信息,请参阅 ASP.NET Core 中间件

开发人员异常页

WebApplication.CreateBuilder 使用预配置默认值初始化 WebApplicationBuilder 类的新实例。 开发人员异常页在预配置的默认值中启用。 当在开发环境中运行以下代码时,导航到 / 以呈现一个显示异常的友好页面。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core 中间件

下表列出了一些经常与最小 API 一起使用的中间件。

中间件 描述 API
身份验证 提供身份验证支持。 UseAuthentication
授权 提供身份验证支持。 UseAuthorization
CORS 配置跨域资源共享。 UseCors
异常处理程序 全局处理中间件管道引发的异常。 UseExceptionHandler
转接头 将代理标头转发到当前请求。 UseForwardedHeaders
HTTPS 重定向 将所有 HTTP 请求重定向到 HTTPS。 UseHttpsRedirection
HTTP 严格传输安全性 (HSTS) 添加特殊响应标头的安全增强中间件。 UseHsts
请求日志记录 提供对记录 HTTP 请求和响应的支持。 UseHttpLogging
请求超时 支持配置请求超时、全局默认值和每个终结点。 UseRequestTimeouts
W3C 请求日志记录 提供对以 W3C 格式记录 HTTP 请求和响应的支持。 UseW3CLogging
响应缓存 提供对缓存响应的支持。 UseResponseCaching
响应压缩 提供对压缩响应的支持。 UseResponseCompression
会话 提供对管理用户会话的支持。 UseSession
静态文件 为提供静态文件和目录浏览提供支持。 UseStaticFilesUseFileServer
WebSockets 启用 WebSockets 协议。 UseWebSockets

以下各部分介绍请求处理:路由、参数绑定和响应。

路由

配置的 WebApplication 支持 Map{Verb}MapMethods,其中 {Verb} 是一种采用大小写混写格式的 HTTP 方法,如 GetPostPutDelete

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

路由处理程序可以声明要捕获的参数。 当向带有声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,userIdbookId 均为 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 扩展方法有助于组织具有共同前缀的终结点组。 它减少了重复代码,并允许通过对添加终结点元数据RequireAuthorizationWithMetadata 等方法的单一调用来自定义整个终结点组。

例如,以下代码创建两组相似的终结点:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

在此方案中,可以适用 201 Created 结果中 Location 标头的相对地址:

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

第一组终结点将仅匹配前缀为 /public/todos 并且无需任何身份验证即可访问的请求。 第二组终结点将仅匹配前缀为 /private/todos 并且需要身份验证的请求。

QueryPrivateTodos终结点筛选器工厂是一种本地函数,用于修改路由处理程序的 TodoDb 参数,以允许访问和存储专用的 todo 数据。

路由组还支持嵌套组和具有路由参数和约束的复杂前缀模式。 在以下示例中,映射到 user 组的路由处理程序可以捕获外部组前缀中定义的 {org}{group} 路由参数。

前缀也可以为空。 这对于在不更改路由模式的情况下向一组终结点添加终结点元数据或筛选器非常有用。

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

向组添加筛选器或元数据的行为与在添加可能已添加到内部组或特定终结点的任何额外筛选器或元数据之前将它们单独添加到每个终结点的行为相同。

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

在上面的示例中,外部筛选器将在内部筛选器之前记录传入请求,即使它是第二个添加的。 由于筛选器应用于不同的组,因此它们相对于彼此的添加顺序并不重要。 如果应用于相同的组或特定终结点,则筛选器的添加顺序非常重要。

/outer/inner/ 的请求将记录以下内容:

/outer group filter
/inner group filter
MapGet filter

参数绑定

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

支持的绑定源:

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

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

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

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

下表显示了前面的示例中使用的参数与关联的绑定源之间的关系。

参数 绑定源
id 路由值
page 查询字符串
customHeader 标头的值开始缓存响应
service 由依赖项注入提供

HTTP 方法 GETHEADOPTIONSDELETE 不会从正文隐式绑定。 如需从这些 HTTP 方法的主体(如 JSON)进行绑定,可以使用 [FromBody]显式绑定或从 HttpRequest 读取。

以下示例 POST 路由处理程序将主体(如 JSON)的绑定源用于 person 参数:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

上述示例中的参数都自动从请求数据绑定。 为了演示参数绑定的便利性,以下路由处理程序演示如何直接从请求读取请求数据:

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

显式参数绑定

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

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
参数 绑定源
id 名称为 id 的路由值
page 名称为 "p" 的查询字符串
service 由依赖项注入提供
contentType 名称为 "Content-Type" 的标头

注意

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

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

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

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

可选参数

在路由处理程序中声明的参数被视为必需参数:

  • 如果请求与路由匹配,则只有在请求中提供了所有必需的参数时,路由处理程序才会运行。
  • 未能提供所有必需的参数会导致错误。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products BadHttpRequestException:查询字符串中未提供必需的参数“int pageNumber”。
/products/1 HTTP 404 错误,无匹配的路由

若要设置为 pageNumber 可选,请将类型定义为可选,或提供默认值:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products 已返回 1
/products2 已返回 1

前面的可为空默认值适用于所有源:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();

如果未发送请求正文,则前面的代码将使用 null 产品调用方法。

注意:如果提供的数据无效并且参数可为空,则路由处理程序不运行。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products 已返回 1
/products?pageNumber=two BadHttpRequestException:无法从“two”绑定参数 "Nullable<int> pageNumber"
/products/two HTTP 404 错误,无匹配的路由

有关详细信息,请参阅绑定失败部分。

特殊类型

以下类型在绑定时没有显式特性:

  • HttpContext:包含有关当前 HTTP 请求或响应的所有信息的上下文:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: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);
    

将请求正文绑定为 StreamPipeReader

请求正文可以绑定为 StreamPipeReader,以有效支持用户必须处理数据的情况,以及:

  • 将数据存储在 Blob 存储中,或将数据排入队列提供程序的队列。
  • 使用工作进程或云功能处理存储的数据。

例如,数据可能排队到 Azure 队列存储 或存储在 Azure Blob 存储中。

以下代码可实现后台队列:

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

以下代码将请求正文绑定到 Stream

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

以下代码显示完整的 Program.cs 文件:

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • 读取数据时,Stream 是与 HttpRequest.Body 相同的对象。
  • 默认情况下,不缓冲请求正文。 读取正文后,不支持后退。 无法多次读取流。
  • 不能在最小操作处理程序之外使用 StreamPipeReader,因为基础缓冲区将被释放或重用。

使用 IFormFile 和 IFormFileCollection 上传文件

以下代码使用 IFormFileIFormFileCollection 上传文件:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

使用授权标头客户端证书或 cookie 标头支持经过身份验证的文件上传请求。

ASP.NET Core 7.0 中未提供对防伪的内置支持。 防伪在 ASP.NET Core 8.0 及更高版本中可用。 但是,这可以使用 IAntiforgery 服务来实现。

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

下面的代码展示如何将查询字符串绑定到基元类型、字符串数组和 StringValues 数组:

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

在类型实现 TryParse 时,支持将查询字符串或标头值绑定到复杂类型的数组。 以下代码绑定到字符串数组,并返回具有指定标记的所有项:

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

以下代码显示了模型和所需的 TryParse 实现:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

以下代码绑定到 int 数组:

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

若要测试上述代码,请添加以下终结点以使用 Todo 项填充数据库:

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

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

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

以下代码绑定到标头键 X-Todo-Id,并返回具有匹配 Id 值的 Todo 项:

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

注意

从查询字符串绑定 string[] 时,缺少任何匹配的查询字符串值都会导致空数组而不是 null 值。

使用 [AsParameters] 对参数列表进行参数绑定

AsParametersAttribute 启用对类型的简单参数绑定而不是复杂或递归模型绑定。

考虑下列代码:

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

请考虑以下 GET 终结点:

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

以下 struct 可用于替换上述突出显示的参数:

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

重构的 GET 终结点将上述 structAsParameters 属性一起使用:

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

structAsParameters 一起使用可能比使用 record 类型性能更佳。

AspNetCore.Docs.Samples 存储库中的完整示例代码

自定义绑定

自定义参数绑定有两种方法:

  1. 对于路由、查询和标头绑定源,通过添加类型的静态 TryParse 方法来绑定自定义类型。
  2. 通过对类型实现 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.1Point: 12.3, 10.1

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync 具有以下 API:

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

下面的代码显示带有 URI /products?SortBy=xyz&SortDir=Desc&Page=99SortBy:xyz, SortDirection:Desc, CurrentPage:99

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

绑定失败

绑定失败时,框架会记录调试消息,并根据失败模式将各种状态代码返回到客户端。

故障模式 可为空参数类型 绑定源 状态代码
{ParameterType}.TryParse 返回 false route/query/header 400
{ParameterType}.BindAsync 返回 null 自定义 400
{ParameterType}.BindAsync 引发 不重要 自定义 500
未能反序列化 JSON 正文 不重要 body 400
错误的内容类型(不是 application/json 不重要 body 415

绑定优先级

用于从参数确定绑定源的规则:

  1. 按以下顺序在参数(From* 属性)上定义的显式属性:
    1. 路由值:[FromRoute]
    2. 查询字符串:[FromQuery]
    3. 标头:[FromHeader]
    4. 正文:[FromBody]
    5. 一个服务:[FromServices]
    6. 参数值:[AsParameters]
  2. 特殊类型
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormFileCollection (HttpContext.Request.Form.Files)
    7. IFormFile (HttpContext.Request.Form.Files[paramName])
    8. Stream (HttpContext.Request.Body)
    9. PipeReader (HttpContext.Request.BodyReader)
  3. 参数类型具有有效的静态 BindAsync 方法。
  4. 参数类型为字符串或具有有效的静态 TryParse 方法。
    1. 如果路由模板中存在参数名称(例如 app.Map("/todo/{id}", (int id) => {});),则会将其从路由中绑定。
    2. 从查询字符串进行绑定。
  5. 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
  6. 参数来自正文。

为正文绑定配置 JSON 反序列化选项

正文绑定源使用 System.Text.Json 进行反序列化。 不能更改此默认值,但可以配置 JSON 序列化和反序列化选项

全局配置 JSON 反序列化选项

全局应用于应用的选项可以通过调用 ConfigureHttpJsonOptions 进行配置。 以下示例包括公共字段,并设置 JSON 输出的格式。

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

由于示例代码同时配置序列化和反序列化,因此它可以读取 NameField 并在输出 JSON 中包含 NameField

为终结点配置 JSON 反序列化选项

ReadFromJsonAsync 具有接受 JsonSerializerOptions 对象的重载。 以下示例包括公共字段,并设置 JSON 输出的格式。

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

由于上述代码仅将自定义选项应用于反序列化,输出 JSON 不包括 NameField

读取请求正文

使用 HttpContextHttpRequest 参数直接读取请求正文:

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();

前面的代码:

响应

路由处理程序支持以下类型的返回值:

  1. 基于 IResult - 这包括 Task<IResult>ValueTask<IResult>
  2. ValueTask<string> - 这包括 stringTask<string>
  3. T(任何其他类型)- 这包括 Task<T>ValueTask<T>
返回值 行为 Content-Type
IResult 框架调用 IResult.ExecuteAsync IResult 实现决定
string 框架将字符串直接写入响应 text/plain
T(任何其他类型) 框架 JSON 序列化响应 application/json

有关路由处理程序返回值的更深入指南,请参阅在最小 API 应用程序中创建响应

示例返回值

字符串返回值

app.MapGet("/hello", () => "Hello World");

JSON 返回值

app.MapGet("/hello", () => new { Message = "Hello World" });

返回 TypedResults

以下代码将返回 TypedResults

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

优先返回 TypedResults,而不是 Results。 有关详细信息,请参阅 TypedResults 与 Results

IResult 返回值

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

以下示例使用内置结果类型自定义响应:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

自定义状态代码

app.MapGet("/405", () => Results.StatusCode(405));

文本

app.MapGet("/text", () => Results.Text("This is some text"));

Stream

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

有关更多示例,请参阅在最小 API 应用程序中创建响应

重定向

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

文件

app.MapGet("/download", () => Results.File("myfile.text"));

内置结果

ResultsTypedResults 静态类中存在常见的结果帮助程序。 优先返回 TypedResults,而不是 Results。 有关详细信息,请参阅 TypedResults 与 Results

自定义结果

应用程序可以通过实现自定义 IResult 类型来控制响应。 以下代码是 HTML 结果类型的示例:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

建议将扩展方法添加到 Microsoft.AspNetCore.Http.IResultExtensions,使这些自定义结果更容易发现。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

类型化结果

IResult 接口可以表示从最小 API 返回的值,这些 API 不利用对 JSON 将返回的对象序列化为 HTTP 响应的隐式支持。 静态 Results 类用于创建各种 IResult 对象,这些对象表示不同类型的响应。 例如,设置响应状态代码或重定向到另一个 URL。

实现 IResult 的类型是公共类型,允许在测试时使用类型断言。 例如:

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

可以查看静态 TypedResults 类上相应方法的返回类型,以找到要转换为的正确公共 IResult 类型。

有关更多示例,请参阅在最小 API 应用程序中创建响应

筛选器

请参阅最小 API 筛选器应用

授权

可以使用授权策略保护路由。 可以通过 [Authorize] 属性或通过使用 RequireAuthorization 方法声明这些对象:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

前面的代码可以用 RequireAuthorization 编写:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

以下示例使用基于策略的授权

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

允许未经身份验证的用户访问终结点

[AllowAnonymous] 允许未经身份验证的用户访问终结点:

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

路由可以使用 CORS 策略启用 CORS。 可以通过 [EnableCors] 属性或通过使用 RequireCors 方法声明 CORS。 以下示例启用 CORS:

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS)

另请参阅

此文档:

最小的 API 包括:

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

多个端口

在以下代码中,应用响应端口 30004000

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

app.MapGet("/", () => "Hello World");

app.Run();

从命令行设置端口

以下命令使应用响应端口 7777

dotnet run --urls="https://localhost:7777"

如果在 appsettings.json 文件中也配置了 Kestrel 终结点,则使用 appsettings.json 文件指定的 URL。 有关详细信息,请参阅Kestrel终结点配置

从环境中读取端口

以下代码从环境中读取端口:

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

app.MapGet("/", () => "Hello World");

app.Run($"http://localhost:{port}");

从环境设置端口的首选方法为使用 ASPNETCORE_URLS 环境变量,如以下部分所示。

通过 ASPNETCORE_URLS 环境变量设置端口

ASPNETCORE_URLS 环境变量可用于设置端口:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS 支持多个 URL:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

侦听所有接口

以下示例演示如何侦听所有接口

http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

app.MapGet("/", () => "Hello World");

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

app.MapGet("/", () => "Hello World");

app.Run();

使用 ASPNETCORE_URLS 侦听所有接口

前面的示例可以使用 ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

指定使用开发证书的 HTTPS

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

有关开发证书详细信息,请参阅在 Windows 和 macOS 上信任 ASP.NET Core HTTPS 开发证书

指定使用自定义证书的 HTTPS

以下部分显示如何使用 appsettings.json 文件和通过配置指定自定义证书。

使用 appsettings.json 指定自定义证书

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

通过配置指定自定义证书

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

使用证书 API

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

app.MapGet("/", () => "Hello World");

app.Run();

读取环境

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

有关使用环境的详细信息,请参阅在 ASP.NET Core 中使用多个环境

配置

以下代码从配置系统读取:

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

有关详细信息,请参阅 ASP.NET Core 中的配置

Logging

以下代码在应用程序启动时将消息写入日志:

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

app.MapGet("/", () => "Hello World");

app.Run();

有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录

访问依赖项注入 (DI) 容器

下面的代码演示如何在应用程序启动过程中从 DI 容器获取服务:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

有关详细信息,请参阅 ASP.NET Core 中的依赖项注入

WebApplicationBuilder

本部分包含使用 WebApplicationBuilder 的示例代码。

更改内容根、应用程序名称和环境

以下代码设置内容根、应用程序名称和环境:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder 使用预配置的默认值初始化 WebApplicationBuilder 类的新实例。

有关详细信息,请参阅 ASP.NET Core 基础知识概述

按环境变量或命令行更改内容根、应用程序名称和环境

下表显示了用于更改内容根、应用程序名称和环境的环境变量及命令行参数:

feature 环境变量 命令行参数
应用程序名称 ASPNETCORE_APPLICATIONNAME --applicationName
环境名称 ASPNETCORE_ENVIRONMENT --environment
内容根 ASPNETCORE_CONTENTROOT --contentRoot

添加配置提供程序

以下示例添加 INI 配置提供程序:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

有关详细信息,请参阅 ASP.NET Core 中的配置中的文件配置提供程序

读取配置

默认情况下,WebApplicationBuilder 从多个源读取配置,包括:

  • appSettings.jsonappSettings.{environment}.json
  • 环境变量
  • 命令行

有关读取的配置源的完整列表,请参阅 ASP.NET Core 中的配置中的默认配置

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

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

读取环境

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

添加日志记录提供程序

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

app.MapGet("/", () => "Hello JSON console!");

app.Run();

添加服务

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

自定义 IHostBuilder

可以使用 IHostBuilder访问 IHostBuilder 上的现有扩展方法:

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.Run();

自定义 IWebHostBuilder

可以使用 IWebHostBuilder 属性访问 IWebHostBuilder 上的扩展方法。

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

更改 Web 根

默认情况下,Web 根相对于 wwwroot 文件夹中的内容根。 Web 根是静态文件中间件查找静态文件的位置。 可以使用 WebHostOptions、命令行或 UseWebRoot 方法更改 Web 根:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

自定义依赖项注入 (DI) 容器

下面的示例使用 Autofac

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

添加中间件

可以在 WebApplication 上配置任何现有的 ASP.NET Core 中间件:

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

app.MapGet("/", () => "Hello World!");

app.Run();

有关详细信息,请参阅 ASP.NET Core 中间件

开发人员异常页

WebApplication.CreateBuilder 使用预配置默认值初始化 WebApplicationBuilder 类的新实例。 开发人员异常页在预配置的默认值中启用。 当在开发环境中运行以下代码时,导航到 / 以呈现一个显示异常的友好页面。

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core 中间件

下表列出了一些经常与最小 API 一起使用的中间件。

中间件 描述 API
身份验证 提供身份验证支持。 UseAuthentication
授权 提供身份验证支持。 UseAuthorization
CORS 配置跨域资源共享。 UseCors
异常处理程序 全局处理中间件管道引发的异常。 UseExceptionHandler
转接头 将代理标头转发到当前请求。 UseForwardedHeaders
HTTPS 重定向 将所有 HTTP 请求重定向到 HTTPS。 UseHttpsRedirection
HTTP 严格传输安全性 (HSTS) 添加特殊响应标头的安全增强中间件。 UseHsts
请求日志记录 提供对记录 HTTP 请求和响应的支持。 UseHttpLogging
W3C 请求日志记录 提供对以 W3C 格式记录 HTTP 请求和响应的支持。 UseW3CLogging
响应缓存 提供对缓存响应的支持。 UseResponseCaching
响应压缩 提供对压缩响应的支持。 UseResponseCompression
会话 提供对管理用户会话的支持。 UseSession
静态文件 为提供静态文件和目录浏览提供支持。 UseStaticFilesUseFileServer
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

路由处理程序可以声明要捕获的参数。 当使用声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,userIdbookId 均为 int

在前面的代码中,如果某个路由值无法转换为 int,则会引发异常。 GET 请求 /users/hello/books/3 引发了以下异常:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

通配符和“全部捕获”路由

以下“全部捕获”路由从“/posts/hello”终结点返回 Routing to hello

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

路由约束

路由约束限制路由的匹配行为。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

下表显示了前面的路由模板及其行为:

路由模板 示例匹配 URI
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

有关详细信息,请参阅 ASP.NET Core 中的路由中的路由约束参考

参数绑定

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

支持的绑定源:

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

注意

.NET 不支持从窗体值进行绑定。

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

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

下表显示了前面的示例中使用的参数与关联的绑定源之间的关系。

参数 绑定源
id 路由值
page 查询字符串
customHeader 标头的值开始缓存响应
service 由依赖项注入提供

HTTP 方法 GETHEADOPTIONSDELETE 不会从正文隐式绑定。 如需从这些 HTTP 方法的主体(如 JSON)进行绑定,可以使用 [FromBody]显式绑定或从 HttpRequest 读取。

以下示例 POST 路由处理程序将主体(如 JSON)的绑定源用于 person 参数:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

上述示例中的参数都自动从请求数据绑定。 为了演示参数绑定的便利性,以下示例路由处理程序演示如何直接从请求读取请求数据:

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

显式参数绑定

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

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
参数 绑定源
id 名称为 id 的路由值
page 名称为 "p" 的查询字符串
service 由依赖项注入提供
contentType 名称为 "Content-Type" 的标头

注意

.NET 不支持从窗体值进行绑定。

与 DI 的参数绑定

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

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

可选参数

在路由处理程序中声明的参数被视为必需参数:

  • 如果请求与路由匹配,则只有在请求中提供了所有必需的参数时,路由处理程序才会运行。
  • 未能提供所有必需的参数会导致错误。
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products BadHttpRequestException:查询字符串中未提供必需的参数“int pageNumber”。
/products/1 HTTP 404 错误,无匹配的路由

若要设置为 pageNumber 可选,请将类型定义为可选,或提供默认值:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products 已返回 1
/products2 已返回 1

前面的可为空默认值适用于所有源:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();

如果未发送请求正文,则前面的代码将使用 null 产品调用方法。

注意:如果提供的数据无效并且参数可为空,则路由处理程序不运行。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI result
/products?pageNumber=3 已返回 3
/products 已返回 1
/products?pageNumber=two BadHttpRequestException:无法从“two”绑定参数 "Nullable<int> pageNumber"
/products/two HTTP 404 错误,无匹配的路由

有关详细信息,请参阅绑定失败部分。

特殊类型

以下类型在绑定时没有显式特性:

  • HttpContext:包含有关当前 HTTP 请求或响应的所有信息的上下文:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequestHttpResponse: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);
    

自定义绑定

自定义参数绑定有两种方法:

  1. 对于路由、查询和标头绑定源,通过添加类型的静态 TryParse 方法来绑定自定义类型。
  2. 通过对类型实现 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.1Point: 12.3, 10.1

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync 具有以下 API:

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

下面的代码显示带有 URI /products?SortBy=xyz&SortDir=Desc&Page=99SortBy:xyz, SortDirection:Desc, CurrentPage:99

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

绑定失败

绑定失败时,框架会记录调试消息,并根据失败模式将各种状态代码返回到客户端。

故障模式 可为空参数类型 绑定源 状态代码
{ParameterType}.TryParse 返回 false route/query/header 400
{ParameterType}.BindAsync 返回 null 自定义 400
{ParameterType}.BindAsync 引发 不重要 自定义 500
未能反序列化 JSON 正文 不重要 body 400
错误的内容类型(不是 application/json 不重要 body 415

绑定优先级

用于从参数确定绑定源的规则:

  1. 按以下顺序在参数(From* 属性)上定义的显式属性:
    1. 路由值:[FromRoute]
    2. 查询字符串:[FromQuery]
    3. 标头:[FromHeader]
    4. 正文:[FromBody]
    5. 服务:[FromServices]
  2. 特殊类型
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
  3. 参数类型具有有效的 BindAsync 方法。
  4. 参数类型为字符串或具有有效的 TryParse 方法。
    1. 如果路由模板中存在参数名称(例如 app.Map("/todo/{id}", (int id) => {});),则会将其从路由中绑定。
    2. 从查询字符串进行绑定。
  5. 如果参数类型为依赖项注入提供的服务,则它将该服务用作源。
  6. 参数来自正文。

自定义 JSON 绑定

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

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/products", (Product product) => product);

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}

前面的代码:

  • 配置输入和输出默认 JSON 选项。
  • 返回下面的 JSON
    {
      "id": 1,
      "name": "Joe Smith"
    }
    
    发布时
    {
      "Id": 1,
      "Name": "Joe Smith"
    }
    

读取请求正文

使用 HttpContextHttpRequest 参数直接读取请求正文:

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();

前面的代码:

响应

路由处理程序支持以下类型的返回值:

  1. 基于 IResult - 这包括 Task<IResult>ValueTask<IResult>
  2. ValueTask<string> - 这包括 stringTask<string>
  3. T(任何其他类型)- 这包括 Task<T>ValueTask<T>
返回值 行为 Content-Type
IResult 框架调用 IResult.ExecuteAsync IResult 实现决定
string 框架将字符串直接写入响应 text/plain
T(任何其他类型) 框架将 JSON 序列化响应 application/json

示例返回值

字符串返回值

app.MapGet("/hello", () => "Hello World");

JSON 返回值

app.MapGet("/hello", () => new { Message = "Hello World" });

IResult 返回值

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

以下示例使用内置结果类型自定义响应:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
自定义状态代码
app.MapGet("/405", () => Results.StatusCode(405));
文本
app.MapGet("/text", () => Results.Text("This is some text"));
Stream
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
重定向
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
文件
app.MapGet("/download", () => Results.File("myfile.text"));

内置结果

Microsoft.AspNetCore.Http.Results 静态类中存在常见的结果帮助程序。

说明 响应类型 状态代码 API
使用高级选项编写 JSON 响应 application/json 200 Results.Json
编写 JSON 响应 application/json 200 Results.Ok
编写文本响应 text/plain(默认),可配置 200 Results.Text
将响应编写为字节 application/octet-stream(默认),可配置 200 Results.Bytes
将字节流写入响应 application/octet-stream(默认),可配置 200 Results.Stream
使用 content-disposition 标头将文件流式传输至响应以下载 application/octet-stream(默认),可配置 200 Results.File
将状态代码设置为 404,并包含可选的 JSON 响应 空值 404 Results.NotFound
将状态代码设置为 204 空值 204 Results.NoContent
将状态代码设置为 422,并包含可选的 JSON 响应 空值 422 Results.UnprocessableEntity
将状态代码设置为 400,并包含可选的 JSON 响应 空值 400 Results.BadRequest
将状态代码设置为 409,并包含可选的 JSON 响应 空值 409 Results.Conflict
将问题详细信息 JSON 对象写入响应 空值 500(默认),可配置 Results.Problem
将问题详细信息 JSON 对象写入包含验证错误的响应 空值 空值,可配置 Results.ValidationProblem

自定义结果

应用程序可以通过实现自定义 IResult 类型来控制响应。 以下代码是 HTML 结果类型的示例:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

建议将扩展方法添加到 Microsoft.AspNetCore.Http.IResultExtensions,使这些自定义结果更容易发现。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

授权

可以使用授权策略保护路由。 可以通过 [Authorize] 属性或通过使用 RequireAuthorization 方法声明这些对象:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

前面的代码可以用 RequireAuthorization 编写:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

以下示例使用基于策略的授权

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

允许未经身份验证的用户访问终结点

[AllowAnonymous] 允许未经身份验证的用户访问终结点:

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

路由可以使用 CORS 策略启用 CORS。 可以通过 [EnableCors] 属性或通过使用 RequireCors 方法声明 CORS。 以下示例启用 CORS:

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/",() => "Hello CORS!");

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

var app = builder.Build();
app.UseCors();

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

有关详细信息,请参阅在 ASP.NET Core 中启用跨源请求 (CORS)

另请参阅

最小 API 中的 OpenAPI 支持