最小 API 应用中的路由处理程序

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅本文.NET 9 版本。

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

配置的 WebApplication 支持 Map{Verb}MapMethods,其中 {Verb} 是 Pascal 的 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 route is /hello

注意:终结点名称区分大小写。

终结点名称:

  • 必须全局唯一。
  • 在启用 OpenAPI 支持时用作 OpenAPI 操作 ID。 有关详细信息,请参阅 OpenAPI

路由参数

路由参数可以作为路由模式定义的一部分进行捕获:

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

前面的代码从 URI /users/3/books/7 返回 The user id is 3 and book id is 7

路由处理程序可以声明要捕获的参数。 当向带有声明要捕获的参数的路由发出请求时,将分析参数并将其传递给处理程序。 这样就可以轻松地以类型安全的方式捕获值。 在前面的代码中,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

参数绑定

最小 API 应用程序中的参数绑定详细介绍了有关如何填充路由处理程序参数的规则。

响应

在最小 API 应用程序中创建响应详细介绍了如何将从路由处理程序返回的值转换为响应。